feat: add i18n support for notifications plugin (#31558)
* feat: add i18n support for notifications plugin Signed-off-by: Yi Cai <yicai@redhat.com> * added changeset Signed-off-by: Yi Cai <yicai@redhat.com> * updates Signed-off-by: Yi Cai <yicai@redhat.com> * fix failed e2e checks Signed-off-by: Yi Cai <yicai@redhat.com> * resolved review comments Signed-off-by: Yi Cai <yicai@redhat.com> * fixed some untranslated string issue Signed-off-by: Yi Cai <yicai@redhat.com> * updated report-alpha.api.md Signed-off-by: Yi Cai <yicai@redhat.com> * updated report api Signed-off-by: Yi Cai <yicai@redhat.com> * updated report api Signed-off-by: Yi Cai <yicai@redhat.com> * simplified code Signed-off-by: Yi Cai <yicai@redhat.com> * updated import to use new dependency Signed-off-by: Yi Cai <yicai@redhat.com> * updated report-alpha.api.md Signed-off-by: Yi Cai <yicai@redhat.com> * code clean up Signed-off-by: Yi Cai <yicai@redhat.com> * addressed review comment Signed-off-by: Yi Cai <yicai@redhat.com> --------- Signed-off-by: Yi Cai <yicai@redhat.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications': patch
|
||||
---
|
||||
|
||||
Added i18n support.
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
|
||||
rules: {
|
||||
'@backstage/no-top-level-material-ui-4-imports': 'error',
|
||||
},
|
||||
rules: {
|
||||
'@backstage/no-top-level-material-ui-4-imports': 'error',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# Knip report
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: OverridableFrontendPlugin<
|
||||
@@ -67,5 +68,70 @@ const _default: OverridableFrontendPlugin<
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const notificationsTranslationRef: TranslationRef<
|
||||
'plugin.notifications',
|
||||
{
|
||||
readonly 'table.errors.markAllReadFailed': 'Failed to mark all notifications as read';
|
||||
readonly 'table.pagination.firstTooltip': 'First Page';
|
||||
readonly 'table.pagination.labelDisplayedRows': '{from}-{to} of {count}';
|
||||
readonly 'table.pagination.labelRowsSelect': 'rows';
|
||||
readonly 'table.pagination.lastTooltip': 'Last Page';
|
||||
readonly 'table.pagination.nextTooltip': 'Next Page';
|
||||
readonly 'table.pagination.previousTooltip': 'Previous Page';
|
||||
readonly 'table.emptyMessage': 'No records to display';
|
||||
readonly 'table.bulkActions.markAllRead': 'Mark all read';
|
||||
readonly 'table.bulkActions.markSelectedAsRead': 'Mark selected as read';
|
||||
readonly 'table.bulkActions.returnSelectedAmongUnread': 'Return selected among unread';
|
||||
readonly 'table.bulkActions.saveSelectedForLater': 'Save selected for later';
|
||||
readonly 'table.bulkActions.undoSaveForSelected': 'Undo save for selected';
|
||||
readonly 'table.confirmDialog.title': 'Are you sure?';
|
||||
readonly 'table.confirmDialog.markAllReadDescription': 'Mark <b>all</b> notifications as <b>read</b>.';
|
||||
readonly 'table.confirmDialog.markAllReadConfirmation': 'Mark All';
|
||||
readonly 'filters.view.all': 'All';
|
||||
readonly 'filters.view.label': 'View';
|
||||
readonly 'filters.view.read': 'Read notifications';
|
||||
readonly 'filters.view.saved': 'Saved';
|
||||
readonly 'filters.view.unread': 'Unread notifications';
|
||||
readonly 'filters.title': 'Filters';
|
||||
readonly 'filters.severity.normal': 'Normal';
|
||||
readonly 'filters.severity.high': 'High';
|
||||
readonly 'filters.severity.low': 'Low';
|
||||
readonly 'filters.severity.label': 'Min severity';
|
||||
readonly 'filters.severity.critical': 'Critical';
|
||||
readonly 'filters.topic.label': 'Topic';
|
||||
readonly 'filters.topic.anyTopic': 'Any topic';
|
||||
readonly 'filters.createdAfter.label': 'Sent out';
|
||||
readonly 'filters.createdAfter.placeholder': 'Notifications since';
|
||||
readonly 'filters.createdAfter.last24h': 'Last 24h';
|
||||
readonly 'filters.createdAfter.lastWeek': 'Last week';
|
||||
readonly 'filters.createdAfter.anyTime': 'Any time';
|
||||
readonly 'filters.sortBy.origin': 'Origin';
|
||||
readonly 'filters.sortBy.label': 'Sort by';
|
||||
readonly 'filters.sortBy.placeholder': 'Field to sort by';
|
||||
readonly 'filters.sortBy.newest': 'Newest on top';
|
||||
readonly 'filters.sortBy.oldest': 'Oldest on top';
|
||||
readonly 'filters.sortBy.topic': 'Topic';
|
||||
readonly 'settings.table.origin': 'Origin';
|
||||
readonly 'settings.table.topic': 'Topic';
|
||||
readonly 'settings.title': 'Notification settings';
|
||||
readonly 'settings.errors.useNotificationFormat': 'useNotificationFormat must be used within a NotificationFormatProvider';
|
||||
readonly 'settings.errorTitle': 'Failed to load settings';
|
||||
readonly 'settings.noSettingsAvailable': 'No notification settings available, check back later';
|
||||
readonly 'sidebar.title': 'Notifications';
|
||||
readonly 'sidebar.errors.markAsReadFailed': 'Failed to mark notification as read';
|
||||
readonly 'sidebar.errors.fetchNotificationFailed': 'Failed to fetch notification';
|
||||
readonly 'notificationsPage.title': 'Notifications';
|
||||
readonly 'notificationsPage.tableTitle.all_one': 'All notifications ({{count}})';
|
||||
readonly 'notificationsPage.tableTitle.all_other': 'All notifications ({{count}})';
|
||||
readonly 'notificationsPage.tableTitle.saved_one': 'Saved notifications ({{count}})';
|
||||
readonly 'notificationsPage.tableTitle.saved_other': 'Saved notifications ({{count}})';
|
||||
readonly 'notificationsPage.tableTitle.unread_one': 'Unread notifications ({{count}})';
|
||||
readonly 'notificationsPage.tableTitle.unread_other': 'Unread notifications ({{count}})';
|
||||
readonly 'notificationsPage.tableTitle.read_one': 'Read notifications ({{count}})';
|
||||
readonly 'notificationsPage.tableTitle.read_other': 'Read notifications ({{count}})';
|
||||
}
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Notification as Notification_2 } from '@backstage/plugin-notifications-
|
||||
import { NotificationSettings } from '@backstage/plugin-notifications-common';
|
||||
import { NotificationSeverity } from '@backstage/plugin-notifications-common';
|
||||
import { NotificationStatus } from '@backstage/plugin-notifications-common';
|
||||
import * as React_2 from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { TableProps } from '@backstage/core-components';
|
||||
|
||||
@@ -111,10 +110,10 @@ export type NotificationSnackbarProperties = {
|
||||
};
|
||||
dense?: boolean;
|
||||
maxSnack?: number;
|
||||
snackStyle?: React_2.CSSProperties;
|
||||
iconVariant?: Partial<Record<NotificationSeverity, React_2.ReactNode>>;
|
||||
snackStyle?: React.CSSProperties;
|
||||
iconVariant?: Partial<Record<NotificationSeverity, React.ReactNode>>;
|
||||
Components?: {
|
||||
[key in NotificationSeverity]: React_2.JSXElementConstructor<any>;
|
||||
[key in NotificationSeverity]: React.JSXElementConstructor<any>;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -55,3 +55,5 @@ export default createFrontendPlugin({
|
||||
// TODO(Rugvip): Nav item (i.e. NotificationsSidebarItem) currently needs to be installed manually
|
||||
extensions: [page, api],
|
||||
});
|
||||
|
||||
export { notificationsTranslationRef } from './translation';
|
||||
|
||||
+67
-55
@@ -15,10 +15,14 @@
|
||||
*/
|
||||
import { ChangeEvent } from 'react';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
import { GetNotificationsOptions } from '../../api';
|
||||
import { NotificationSeverity } from '@backstage/plugin-notifications-common';
|
||||
|
||||
@@ -44,58 +48,53 @@ export type NotificationsFiltersProps = {
|
||||
|
||||
const ALL = '___all___';
|
||||
|
||||
export const CreatedAfterOptions: {
|
||||
[key: string]: { label: string; getDate: () => Date };
|
||||
} = {
|
||||
type TranslationKey = keyof (typeof notificationsTranslationRef)['T'];
|
||||
|
||||
export const CreatedAfterOptions = {
|
||||
last24h: {
|
||||
label: 'Last 24h',
|
||||
labelKey: 'filters.createdAfter.last24h' satisfies TranslationKey,
|
||||
getDate: () => new Date(Date.now() - 24 * 3600 * 1000),
|
||||
},
|
||||
lastWeek: {
|
||||
label: 'Last week',
|
||||
labelKey: 'filters.createdAfter.lastWeek' satisfies TranslationKey,
|
||||
getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000),
|
||||
},
|
||||
all: {
|
||||
label: 'Any time',
|
||||
labelKey: 'filters.createdAfter.anyTime' satisfies TranslationKey,
|
||||
getDate: () => new Date(0),
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const SortByOptions: {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
sortBy: SortBy;
|
||||
};
|
||||
} = {
|
||||
export const SortByOptions = {
|
||||
newest: {
|
||||
label: 'Newest on top',
|
||||
labelKey: 'filters.sortBy.newest' satisfies TranslationKey,
|
||||
sortBy: {
|
||||
sort: 'created',
|
||||
sortOrder: 'desc',
|
||||
sort: 'created' as const,
|
||||
sortOrder: 'desc' as const,
|
||||
},
|
||||
},
|
||||
oldest: {
|
||||
label: 'Oldest on top',
|
||||
labelKey: 'filters.sortBy.oldest' satisfies TranslationKey,
|
||||
sortBy: {
|
||||
sort: 'created',
|
||||
sortOrder: 'asc',
|
||||
sort: 'created' as const,
|
||||
sortOrder: 'asc' as const,
|
||||
},
|
||||
},
|
||||
topic: {
|
||||
label: 'Topic',
|
||||
labelKey: 'filters.sortBy.topic' satisfies TranslationKey,
|
||||
sortBy: {
|
||||
sort: 'topic',
|
||||
sortOrder: 'asc',
|
||||
sort: 'topic' as const,
|
||||
sortOrder: 'asc' as const,
|
||||
},
|
||||
},
|
||||
origin: {
|
||||
label: 'Origin',
|
||||
labelKey: 'filters.sortBy.origin' satisfies TranslationKey,
|
||||
sortBy: {
|
||||
sort: 'origin',
|
||||
sortOrder: 'asc',
|
||||
sort: 'origin' as const,
|
||||
sortOrder: 'asc' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
const getSortByText = (sortBy?: SortBy): string => {
|
||||
if (sortBy?.sort === 'created' && sortBy?.sortOrder === 'asc') {
|
||||
@@ -111,13 +110,6 @@ const getSortByText = (sortBy?: SortBy): string => {
|
||||
return 'newest';
|
||||
};
|
||||
|
||||
const AllSeverityOptions: { [key in NotificationSeverity]: string } = {
|
||||
critical: 'Critical',
|
||||
high: 'High',
|
||||
normal: 'Normal',
|
||||
low: 'Low',
|
||||
};
|
||||
|
||||
export const NotificationsFilters = ({
|
||||
sorting,
|
||||
onSortingChanged,
|
||||
@@ -133,6 +125,7 @@ export const NotificationsFilters = ({
|
||||
onTopicChanged,
|
||||
allTopics,
|
||||
}: NotificationsFiltersProps) => {
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const sortByText = getSortByText(sorting);
|
||||
|
||||
const handleOnCreatedAfterChanged = (
|
||||
@@ -163,7 +156,8 @@ export const NotificationsFilters = ({
|
||||
const handleOnSortByChanged = (
|
||||
event: ChangeEvent<{ name?: string; value: unknown }>,
|
||||
) => {
|
||||
const idx = (event.target.value as string) || 'newest';
|
||||
const idx = ((event.target.value as string) ||
|
||||
'newest') as keyof typeof SortByOptions;
|
||||
const option = SortByOptions[idx];
|
||||
onSortingChanged({ ...option.sortBy });
|
||||
};
|
||||
@@ -197,37 +191,49 @@ export const NotificationsFilters = ({
|
||||
return (
|
||||
<>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">{t('filters.title')}</Typography>
|
||||
<Divider variant="fullWidth" />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-view">View</InputLabel>
|
||||
<InputLabel id="notifications-filter-view">
|
||||
{t('filters.view.label')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="notifications-filter-view"
|
||||
label="View"
|
||||
label={t('filters.view.label')}
|
||||
value={viewValue}
|
||||
onChange={handleOnViewChanged}
|
||||
>
|
||||
<MenuItem value="unread">Unread notifications</MenuItem>
|
||||
<MenuItem value="read">Read notifications</MenuItem>
|
||||
<MenuItem value="saved">Saved</MenuItem>
|
||||
<MenuItem value="all">All</MenuItem>
|
||||
<MenuItem value="unread">{t('filters.view.unread')}</MenuItem>
|
||||
<MenuItem value="read">{t('filters.view.read')}</MenuItem>
|
||||
<MenuItem value="saved">{t('filters.view.saved')}</MenuItem>
|
||||
<MenuItem value="all">{t('filters.view.all')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-created">Sent out</InputLabel>
|
||||
<InputLabel id="notifications-filter-created">
|
||||
{t('filters.createdAfter.label')}
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
label="Sent out"
|
||||
label={t('filters.createdAfter.label')}
|
||||
labelId="notifications-filter-created"
|
||||
placeholder="Notifications since"
|
||||
placeholder={t('filters.createdAfter.placeholder')}
|
||||
value={createdAfter}
|
||||
onChange={handleOnCreatedAfterChanged}
|
||||
>
|
||||
{Object.keys(CreatedAfterOptions).map((key: string) => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{CreatedAfterOptions[key].label}
|
||||
{t(
|
||||
CreatedAfterOptions[key as keyof typeof CreatedAfterOptions]
|
||||
.labelKey,
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -236,18 +242,20 @@ export const NotificationsFilters = ({
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-sort">Sort by</InputLabel>
|
||||
<InputLabel id="notifications-filter-sort">
|
||||
{t('filters.sortBy.label')}
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
label="Sort by"
|
||||
label={t('filters.sortBy.label')}
|
||||
labelId="notifications-filter-sort"
|
||||
placeholder="Field to sort by"
|
||||
placeholder={t('filters.sortBy.placeholder')}
|
||||
value={sortByText}
|
||||
onChange={handleOnSortByChanged}
|
||||
>
|
||||
{Object.keys(SortByOptions).map((key: string) => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{SortByOptions[key].label}
|
||||
{t(SortByOptions[key as keyof typeof SortByOptions].labelKey)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -257,18 +265,20 @@ export const NotificationsFilters = ({
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-severity">
|
||||
Min severity
|
||||
{t('filters.severity.label')}
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
label="Min severity"
|
||||
label={t('filters.severity.label')}
|
||||
labelId="notifications-filter-severity"
|
||||
value={severity}
|
||||
onChange={handleOnSeverityChanged}
|
||||
>
|
||||
{Object.keys(AllSeverityOptions).map((key: string) => (
|
||||
{(
|
||||
['critical', 'high', 'normal', 'low'] as NotificationSeverity[]
|
||||
).map(key => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{AllSeverityOptions[key as NotificationSeverity]}
|
||||
{t(`filters.severity.${key}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -277,16 +287,18 @@ export const NotificationsFilters = ({
|
||||
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-topic">Topic</InputLabel>
|
||||
<InputLabel id="notifications-filter-topic">
|
||||
{t('filters.topic.label')}
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
label="Topic"
|
||||
label={t('filters.topic.label')}
|
||||
labelId="notifications-filter-topic"
|
||||
value={topic ?? ALL}
|
||||
onChange={handleOnTopicChanged}
|
||||
>
|
||||
<MenuItem value={ALL} key={ALL}>
|
||||
Any topic
|
||||
{t('filters.topic.anyTopic')}
|
||||
</MenuItem>
|
||||
|
||||
{sortedAllTopics.map((item: string) => (
|
||||
|
||||
@@ -24,6 +24,15 @@ import {
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { ConfirmProvider } from 'material-ui-confirm';
|
||||
import { useSignal } from '@backstage/plugin-signals-react';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
|
||||
const TableTitleKeys = {
|
||||
all: 'notificationsPage.tableTitle.all',
|
||||
saved: 'notificationsPage.tableTitle.saved',
|
||||
unread: 'notificationsPage.tableTitle.unread',
|
||||
read: 'notificationsPage.tableTitle.read',
|
||||
} as const;
|
||||
|
||||
import { NotificationsTable } from '../NotificationsTable';
|
||||
import { useNotificationsApi } from '../../hooks';
|
||||
@@ -58,8 +67,9 @@ export type NotificationsPageProps = {
|
||||
};
|
||||
|
||||
export const NotificationsPage = (props?: NotificationsPageProps) => {
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const {
|
||||
title = 'Notifications',
|
||||
title = t('notificationsPage.title'),
|
||||
themeId = 'tool',
|
||||
subtitle,
|
||||
tooltip,
|
||||
@@ -101,7 +111,10 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
|
||||
options.topic = topic;
|
||||
}
|
||||
|
||||
const createdAfterDate = CreatedAfterOptions[createdAfter].getDate();
|
||||
const createdAfterDate =
|
||||
CreatedAfterOptions[
|
||||
createdAfter as keyof typeof CreatedAfterOptions
|
||||
].getDate();
|
||||
if (createdAfterDate.valueOf() > 0) {
|
||||
options.createdAfter = createdAfterDate;
|
||||
}
|
||||
@@ -156,17 +169,21 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
|
||||
const isUnread = !!value?.[1]?.unread;
|
||||
const allTopics = value?.[2]?.topics;
|
||||
|
||||
let tableTitle = `All notifications `;
|
||||
let tableTitle: string = t(TableTitleKeys.all, {
|
||||
count: totalCount ?? 0,
|
||||
});
|
||||
if (saved) {
|
||||
tableTitle = `Saved notifications`;
|
||||
tableTitle = t(TableTitleKeys.saved, {
|
||||
count: totalCount ?? 0,
|
||||
});
|
||||
} else if (unreadOnly === true) {
|
||||
tableTitle = `Unread notifications`;
|
||||
tableTitle = t(TableTitleKeys.unread, {
|
||||
count: totalCount ?? 0,
|
||||
});
|
||||
} else if (unreadOnly === false) {
|
||||
tableTitle = `Read notifications`;
|
||||
}
|
||||
|
||||
if (totalCount) {
|
||||
tableTitle += ` (${totalCount})`;
|
||||
tableTitle = t(TableTitleKeys.read, {
|
||||
count: totalCount ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
-1
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNotificationsApi } from '../../hooks';
|
||||
import { Link, SidebarItem } from '@backstage/core-components';
|
||||
|
||||
@@ -22,6 +22,8 @@ import MarkAsReadIcon from '@material-ui/icons/CheckCircle';
|
||||
import MarkAsUnsavedIcon from '@material-ui/icons/LabelOff' /* TODO: use BookmarkRemove and BookmarkAdd once we have mui 5 icons */;
|
||||
import MarkAsSavedIcon from '@material-ui/icons/Label';
|
||||
import MarkAllReadIcon from '@material-ui/icons/DoneAll';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
|
||||
export const BulkActions = ({
|
||||
selectedNotifications,
|
||||
@@ -38,6 +40,7 @@ export const BulkActions = ({
|
||||
onSwitchSavedStatus: (ids: Notification['id'][], newStatus: boolean) => void;
|
||||
onMarkAllRead?: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const isDisabled = selectedNotifications.size === 0;
|
||||
const bulkNotifications = notifications.filter(notification =>
|
||||
selectedNotifications.has(notification.id),
|
||||
@@ -51,24 +54,25 @@ export const BulkActions = ({
|
||||
);
|
||||
|
||||
const markAsReadText = isOneRead
|
||||
? 'Return selected among unread'
|
||||
: 'Mark selected as read';
|
||||
? t('table.bulkActions.returnSelectedAmongUnread')
|
||||
: t('table.bulkActions.markSelectedAsRead');
|
||||
const IconComponent = isOneRead ? MarkAsUnreadIcon : MarkAsReadIcon;
|
||||
|
||||
const markAsSavedText = isOneSaved
|
||||
? 'Undo save for selected'
|
||||
: 'Save selected for later';
|
||||
? t('table.bulkActions.undoSaveForSelected')
|
||||
: t('table.bulkActions.saveSelectedForLater');
|
||||
const SavedIconComponent = isOneSaved ? MarkAsUnsavedIcon : MarkAsSavedIcon;
|
||||
const markAllReadText = t('table.bulkActions.markAllRead');
|
||||
|
||||
return (
|
||||
<Grid container wrap="nowrap">
|
||||
<Grid item xs={3}>
|
||||
{onMarkAllRead ? (
|
||||
<Tooltip title="Mark all read">
|
||||
<Tooltip title={markAllReadText}>
|
||||
<div>
|
||||
{/* The <div> here is a workaround for the Tooltip which does not work for a "disabled" child */}
|
||||
<IconButton disabled={!isUnread} onClick={onMarkAllRead}>
|
||||
<MarkAllReadIcon aria-label={markAsSavedText} />
|
||||
<MarkAllReadIcon aria-label={markAllReadText} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
TableColumn,
|
||||
TableProps,
|
||||
} from '@backstage/core-components';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
|
||||
import { notificationsApiRef } from '../../api';
|
||||
import { SelectAll } from './SelectAll';
|
||||
@@ -86,6 +88,7 @@ export const NotificationsTable = ({
|
||||
pageSize,
|
||||
totalCount,
|
||||
}: NotificationsTableProps) => {
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const classes = useStyles();
|
||||
const notificationsApi = useApi(notificationsApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
@@ -341,6 +344,19 @@ export const NotificationsTable = ({
|
||||
onSearchChange={throttledContainsTextHandler}
|
||||
data={notifications}
|
||||
columns={compactColumns}
|
||||
localization={{
|
||||
body: {
|
||||
emptyDataSourceMessage: t('table.emptyMessage'),
|
||||
},
|
||||
pagination: {
|
||||
firstTooltip: t('table.pagination.firstTooltip'),
|
||||
labelDisplayedRows: t('table.pagination.labelDisplayedRows'),
|
||||
labelRowsSelect: t('table.pagination.labelRowsSelect'),
|
||||
lastTooltip: t('table.pagination.lastTooltip'),
|
||||
nextTooltip: t('table.pagination.nextTooltip'),
|
||||
previousTooltip: t('table.pagination.previousTooltip'),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-6
@@ -20,6 +20,8 @@ import { useNotificationsApi } from '../../hooks';
|
||||
import { NotificationSettings } from '@backstage/plugin-notifications-common';
|
||||
import { notificationsApiRef } from '../../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { notificationsTranslationRef } from '../../translation';
|
||||
import { UserNotificationSettingsPanel } from './UserNotificationSettingsPanel';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
@@ -33,11 +35,9 @@ const NotificationFormatContext = createContext<FormatContextType | undefined>(
|
||||
);
|
||||
|
||||
export const useNotificationFormat = () => {
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const context = useContext(NotificationFormatContext);
|
||||
if (!context)
|
||||
throw new Error(
|
||||
'useNotificationFormat must be used within a NotificationFormatProvider',
|
||||
);
|
||||
if (!context) throw new Error(t('settings.errors.useNotificationFormat'));
|
||||
return context;
|
||||
};
|
||||
|
||||
@@ -83,6 +83,7 @@ export const UserNotificationSettingsCard = (props: {
|
||||
originNames?: Record<string, string>;
|
||||
topicNames?: Record<string, string>;
|
||||
}) => {
|
||||
const { t } = useTranslationRef(notificationsTranslationRef);
|
||||
const [settings, setNotificationSettings] = useState<
|
||||
NotificationSettings | undefined
|
||||
>(undefined);
|
||||
@@ -105,9 +106,9 @@ export const UserNotificationSettingsCard = (props: {
|
||||
};
|
||||
|
||||
return (
|
||||
<InfoCard title="Notification settings" variant="gridItem">
|
||||
<InfoCard title={t('settings.title')} variant="gridItem">
|
||||
{loading && <Progress />}
|
||||
{error && <ErrorPanel title="Failed to load settings" error={error} />}
|
||||
{error && <ErrorPanel title={t('settings.errorTitle')} error={error} />}
|
||||
{settings && (
|
||||
<NotificationFormatProvider
|
||||
originMap={props.originNames}
|
||||
|
||||
+4
-3
@@ -13,12 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { UserNotificationSettingsPanel } from './UserNotificationSettingsPanel';
|
||||
import { NotificationFormatProvider } from './UserNotificationSettingsCard.tsx';
|
||||
|
||||
describe('UserNotificationSettingsPanel', () => {
|
||||
it('renders each origin only once even if present in multiple channels', () => {
|
||||
it('renders each origin only once even if present in multiple channels', async () => {
|
||||
const settings = {
|
||||
channels: [
|
||||
{
|
||||
@@ -43,7 +44,7 @@ describe('UserNotificationSettingsPanel', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<NotificationFormatProvider originMap={{}} topicMap={{}}>
|
||||
<UserNotificationSettingsPanel
|
||||
settings={settings}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* 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 { createTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
/** @alpha */
|
||||
export const notificationsTranslationRef = createTranslationRef({
|
||||
id: 'plugin.notifications',
|
||||
messages: {
|
||||
notificationsPage: {
|
||||
title: 'Notifications',
|
||||
tableTitle: {
|
||||
all_one: 'All notifications ({{count}})',
|
||||
all_other: 'All notifications ({{count}})',
|
||||
saved_one: 'Saved notifications ({{count}})',
|
||||
saved_other: 'Saved notifications ({{count}})',
|
||||
unread_one: 'Unread notifications ({{count}})',
|
||||
unread_other: 'Unread notifications ({{count}})',
|
||||
read_one: 'Read notifications ({{count}})',
|
||||
read_other: 'Read notifications ({{count}})',
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
title: 'Filters',
|
||||
view: {
|
||||
label: 'View',
|
||||
unread: 'Unread notifications',
|
||||
read: 'Read notifications',
|
||||
saved: 'Saved',
|
||||
all: 'All',
|
||||
},
|
||||
createdAfter: {
|
||||
label: 'Sent out',
|
||||
placeholder: 'Notifications since',
|
||||
last24h: 'Last 24h',
|
||||
lastWeek: 'Last week',
|
||||
anyTime: 'Any time',
|
||||
},
|
||||
sortBy: {
|
||||
label: 'Sort by',
|
||||
placeholder: 'Field to sort by',
|
||||
newest: 'Newest on top',
|
||||
oldest: 'Oldest on top',
|
||||
topic: 'Topic',
|
||||
origin: 'Origin',
|
||||
},
|
||||
severity: {
|
||||
label: 'Min severity',
|
||||
critical: 'Critical',
|
||||
high: 'High',
|
||||
normal: 'Normal',
|
||||
low: 'Low',
|
||||
},
|
||||
topic: {
|
||||
label: 'Topic',
|
||||
anyTopic: 'Any topic',
|
||||
},
|
||||
},
|
||||
table: {
|
||||
emptyMessage: 'No records to display',
|
||||
pagination: {
|
||||
firstTooltip: 'First Page',
|
||||
labelDisplayedRows: '{from}-{to} of {count}',
|
||||
labelRowsSelect: 'rows',
|
||||
lastTooltip: 'Last Page',
|
||||
nextTooltip: 'Next Page',
|
||||
previousTooltip: 'Previous Page',
|
||||
},
|
||||
bulkActions: {
|
||||
markAllRead: 'Mark all read',
|
||||
markSelectedAsRead: 'Mark selected as read',
|
||||
returnSelectedAmongUnread: 'Return selected among unread',
|
||||
saveSelectedForLater: 'Save selected for later',
|
||||
undoSaveForSelected: 'Undo save for selected',
|
||||
},
|
||||
confirmDialog: {
|
||||
title: 'Are you sure?',
|
||||
markAllReadDescription: 'Mark <b>all</b> notifications as <b>read</b>.',
|
||||
markAllReadConfirmation: 'Mark All',
|
||||
},
|
||||
errors: {
|
||||
markAllReadFailed: 'Failed to mark all notifications as read',
|
||||
},
|
||||
},
|
||||
sidebar: {
|
||||
title: 'Notifications',
|
||||
errors: {
|
||||
markAsReadFailed: 'Failed to mark notification as read',
|
||||
fetchNotificationFailed: 'Failed to fetch notification',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
title: 'Notification settings',
|
||||
errorTitle: 'Failed to load settings',
|
||||
noSettingsAvailable:
|
||||
'No notification settings available, check back later',
|
||||
table: {
|
||||
origin: 'Origin',
|
||||
topic: 'Topic',
|
||||
},
|
||||
errors: {
|
||||
useNotificationFormat:
|
||||
'useNotificationFormat must be used within a NotificationFormatProvider',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user