From 5ba58b18701656e757be4bce5686e09bf3e82de5 Mon Sep 17 00:00:00 2001 From: kandl Date: Thu, 19 Dec 2024 10:53:59 +0100 Subject: [PATCH] fix: show notification panel on mobile - Show notification panel in mobile view - Comment out filtering details for now - Close details panel when opening another one risk: low JIRA: F1-990 --- .../backend/workspace/automations/index.ts | 2 + .../initializeAutomationsHandler.ts | 63 ++++++---- libs/sdk-ui-ext/api/sdk-ui-ext.api.md | 1 + .../src/internal/translations/en-US.json | 2 +- .../Notification/AlertNotification.tsx | 29 +++-- .../NotificationFiltersDetail.tsx | 2 +- .../NotificationTriggersDetail.tsx | 10 +- .../DefaultNotificationsList.scss | 3 + .../DefaultNotificationsList.tsx | 28 ++++- .../DefaultNotificationsPanel.scss | 11 +- .../DefaultNotificationsPanelHeader.scss | 7 +- .../NotificationsPanel/NotificationsPanel.tsx | 115 +++++++++++------- .../components/VirtualList.scss | 6 + .../components/VirtualList.tsx | 12 +- .../data/useFetchNotifications.ts | 9 ++ .../data/useNotificationFiltersDetail.tsx | 54 ++++++-- .../data/useNotifications.tsx | 11 +- libs/sdk-ui-kit/api/sdk-ui-kit.api.md | 13 +- libs/sdk-ui-kit/src/@ui/UiButton/UiButton.tsx | 3 + libs/sdk-ui-kit/src/Header/Header.tsx | 103 +++++++++++++--- libs/sdk-ui-kit/src/Header/HeaderHelp.tsx | 2 + libs/sdk-ui-kit/src/Header/HeaderMenu.tsx | 3 +- libs/sdk-ui-kit/src/Header/typings.ts | 7 +- libs/sdk-ui-kit/styles/scss/header.scss | 70 ++++++++++- .../src/base/localization/bundles/en-US.json | 5 + 25 files changed, 444 insertions(+), 127 deletions(-) diff --git a/libs/sdk-backend-tiger/src/backend/workspace/automations/index.ts b/libs/sdk-backend-tiger/src/backend/workspace/automations/index.ts index b4aea748d5b..e0a190991d4 100644 --- a/libs/sdk-backend-tiger/src/backend/workspace/automations/index.ts +++ b/libs/sdk-backend-tiger/src/backend/workspace/automations/index.ts @@ -29,6 +29,7 @@ export class TigerWorkspaceAutomationService implements IWorkspaceAutomationServ "notificationChannel", "recipients", "exportDefinitions", + "analyticalDashboard", ...(loadUserData ? (["createdBy", "modifiedBy"] as const) : []), ], origin: "NATIVE", // ensures that no inherited automations are returned @@ -60,6 +61,7 @@ export class TigerWorkspaceAutomationService implements IWorkspaceAutomationServ "notificationChannel", "recipients", "exportDefinitions", + "analyticalDashboard", ...(loadUserData ? (["createdBy", "modifiedBy"] as const) : []), ], }); diff --git a/libs/sdk-ui-dashboard/src/model/commandHandlers/scheduledEmail/initializeAutomationsHandler.ts b/libs/sdk-ui-dashboard/src/model/commandHandlers/scheduledEmail/initializeAutomationsHandler.ts index 79e6b8234a1..a9b4e772069 100644 --- a/libs/sdk-ui-dashboard/src/model/commandHandlers/scheduledEmail/initializeAutomationsHandler.ts +++ b/libs/sdk-ui-dashboard/src/model/commandHandlers/scheduledEmail/initializeAutomationsHandler.ts @@ -27,15 +27,17 @@ import { } from "../../store/automations/automationsSelectors.js"; import { selectCanManageWorkspace } from "../../store/permissions/permissionsSelectors.js"; import { + filterLocalIdentifier, filterObjRef, - IAttributeFilter, - IDateFilter, - isDateFilter, - isRelativeDateFilter, + idRef, + IFilter, + IInsight, + insightFilters, } from "@gooddata/sdk-model"; import { changeFilterContextSelectionHandler } from "../filterContext/changeFilterContextSelectionHandler.js"; import { changeFilterContextSelection } from "../../commands/filters.js"; -import omit from "lodash/omit.js"; +import { IDashboardFilter, isDashboardFilter } from "../../../types.js"; +import { selectInsightByWidgetRef } from "../../store/insights/insightsSelectors.js"; export function* initializeAutomationsHandler( ctx: DashboardContext, @@ -90,24 +92,16 @@ export function* initializeAutomationsHandler( if (automationId) { const targetAutomation = automations.find((a) => a.id === automationId); const targetWidget = targetAutomation?.metadata?.widget; - const targetFilters = targetAutomation?.alert?.execution?.filters; - const filtersWithObjRef = targetFilters - ?.filter((f) => { - const objRef = filterObjRef(f); - return !!objRef; - }) - .map((f) => { - if (isDateFilter(f)) { - if (isRelativeDateFilter(f)) { - return omit(f, "relativeDateFilter.dataSet"); - } - return omit(f, "absoluteDateFilter.dataSet"); - } - return f; - }) as (IAttributeFilter | IDateFilter)[]; + const targetFilters = targetAutomation?.alert?.execution?.filters.filter(isDashboardFilter); + if (targetWidget && targetFilters) { + const insight: ReturnType> = yield select( + selectInsightByWidgetRef(idRef(targetWidget)), + ); + const filtersToSet = insight + ? getDashboardFiltersOnly(targetFilters, insight) + : targetFilters; - if (targetWidget && filtersWithObjRef?.length) { - const cmd = changeFilterContextSelection(filtersWithObjRef, true, automationId); + const cmd = changeFilterContextSelection(filtersToSet, true, automationId); yield call(changeFilterContextSelectionHandler, ctx, cmd); } } @@ -132,3 +126,28 @@ export function* initializeAutomationsHandler( ); } } + +/** + * Filter out insight filters from the list of filters + * @internal + */ +function getDashboardFiltersOnly(filters: IFilter[], insight: IInsight) { + return removeAlertFilters(filters).filter((f) => { + const insightFilter = insightFilters(insight).find((f2) => { + return filterLocalIdentifier(f) === filterLocalIdentifier(f2); + }); + + return !insightFilter; + }) as IDashboardFilter[]; +} + +/** + * Remove alert filters (these that are set during creation of the alert sliced by attribute) from the list of filters + * @internal + */ +function removeAlertFilters(filters: IFilter[]) { + return filters?.filter((f) => { + const objRef = filterObjRef(f); + return !!objRef; + }); +} diff --git a/libs/sdk-ui-ext/api/sdk-ui-ext.api.md b/libs/sdk-ui-ext/api/sdk-ui-ext.api.md index 5ea89eccbd2..dd1058c0a83 100644 --- a/libs/sdk-ui-ext/api/sdk-ui-ext.api.md +++ b/libs/sdk-ui-ext/api/sdk-ui-ext.api.md @@ -537,6 +537,7 @@ export interface INotificationsPanelProps extends INotificationsPanelCustomCompo locale?: ILocale; onNotificationClick: (notification: INotification) => void; refreshInterval?: number; + renderInline?: boolean; workspace?: string; } diff --git a/libs/sdk-ui-ext/src/internal/translations/en-US.json b/libs/sdk-ui-ext/src/internal/translations/en-US.json index a5adf882a17..7e38e4f24fc 100644 --- a/libs/sdk-ui-ext/src/internal/translations/en-US.json +++ b/libs/sdk-ui-ext/src/internal/translations/en-US.json @@ -2054,7 +2054,7 @@ "limit": 0 }, "notifications.filters.buttonLabel": { - "value": "{count} filters", + "value": "Show filters", "comment": "", "limit": 0 }, diff --git a/libs/sdk-ui-ext/src/notificationsPanel/Notification/AlertNotification.tsx b/libs/sdk-ui-ext/src/notificationsPanel/Notification/AlertNotification.tsx index fe65906eb1b..9e15d7e9662 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/Notification/AlertNotification.tsx +++ b/libs/sdk-ui-ext/src/notificationsPanel/Notification/AlertNotification.tsx @@ -4,7 +4,7 @@ import { IAlertDescription, IAlertNotification, INotification } from "@gooddata/ import { getDateTimeConfig, IDateConfig, UiIcon } from "@gooddata/sdk-ui-kit"; import { bem } from "../bem.js"; import { Tooltip } from "../components/Tooltip.js"; -import { NotificationFiltersDetail } from "../NotificationFiltersDetail/NotificationFiltersDetail.js"; +// import { NotificationFiltersDetail } from "../NotificationFiltersDetail/NotificationFiltersDetail.js"; import { NotificationTriggerDetail } from "../NotificationTriggersDetail/NotificationTriggersDetail.js"; import { defineMessages, FormattedDate, FormattedMessage, FormattedTime, useIntl } from "react-intl"; @@ -34,13 +34,24 @@ export function AlertNotification({ markAsRead(notification.id); }; - const clickNotification = useCallback(() => { - onNotificationClick(notification); - }, [onNotificationClick, notification]); + const clickNotification = useCallback( + (event: React.MouseEvent) => { + const target = event.target; + const targetIsElement = target instanceof Element; + const isNotificationsDetailsLink = + targetIsElement && target.closest(`[data-id="notification-detail"]`); + if (isNotificationsDetailsLink) { + return; + } + onNotificationClick(notification); + }, + [onNotificationClick, notification], + ); - const filterCount = notification.details.data.alert.filterCount; - const isSliced = notification.details.data.alert.attribute; - const showSeparator = filterCount && filterCount > 0 && isSliced; + // Hide filters for now, as there is lot of unresolved cases to consider + // const filterCount = notification.details.data.alert.filterCount; + // const isSliced = notification.details.data.alert.attribute; + // const showSeparator = filterCount && filterCount > 0 && isSliced; const notificationTitle = getNotificationTitle(notification); return ( @@ -54,8 +65,8 @@ export function AlertNotification({ {notificationTitle}
- - {showSeparator ? "・" : null} + {/* + {showSeparator ? "・" : null} */}
diff --git a/libs/sdk-ui-ext/src/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.tsx b/libs/sdk-ui-ext/src/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.tsx index 8369d3e6d48..9cf0e0cb29e 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.tsx +++ b/libs/sdk-ui-ext/src/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.tsx @@ -49,7 +49,7 @@ export function NotificationFiltersDetail({ notification }: INotificationFilters onClick={onButtonClick} variant="tertiary" size="small" - label={intl.formatMessage(messages.buttonLabel, { count: filterCount })} + label={intl.formatMessage(messages.buttonLabel)} /> ) : null} {isFiltersDialogOpen ? ( diff --git a/libs/sdk-ui-ext/src/notificationsPanel/NotificationTriggersDetail/NotificationTriggersDetail.tsx b/libs/sdk-ui-ext/src/notificationsPanel/NotificationTriggersDetail/NotificationTriggersDetail.tsx index 4240540a6a0..6b2af629621 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/NotificationTriggersDetail/NotificationTriggersDetail.tsx +++ b/libs/sdk-ui-ext/src/notificationsPanel/NotificationTriggersDetail/NotificationTriggersDetail.tsx @@ -11,6 +11,11 @@ const ALIGN_POINTS = [ overlayAlignPoint: "top-right", offset: { x: 2, y: 3 }, }), + alignConfigToAlignPoint({ + triggerAlignPoint: "bottom-left", + overlayAlignPoint: "top-left", + offset: { x: 2, y: 3 }, + }), ]; const messages = defineMessages({ @@ -39,13 +44,13 @@ export function NotificationTriggerDetail({ notification }: INotificationTrigger <> { - e.stopPropagation(); + onClick={() => { toggleTriggersDialog(); }} variant="tertiary" size="small" label={triggersTitle} + dataId="notification-detail" /> {isTriggersDialogOpen ? ( 0; + const containerRef = useRef(null); + const [availableHeight, setAvailableHeight] = useState(0); + + useLayoutEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + const [entry] = entries; + if (entry) { + setAvailableHeight(entry.contentRect.height); + } + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + if (resizeObserver) { + resizeObserver.disconnect(); + setAvailableHeight(0); + } + }; + }, []); + return ( -
+
{isError ? : null} {isEmpty ? : null} {isLoading || isSuccess ? ( @@ -69,6 +92,7 @@ export function DefaultNotificationsList({ hasNextPage={hasNextPage} loadNextPage={loadNextPage} isLoading={isLoading} + maxHeight={Math.max(527, availableHeight)} > {(notification) => (
diff --git a/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanel.scss b/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanel.scss index 73d01ad411a..4262e06174e 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanel.scss +++ b/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanel.scss @@ -9,10 +9,15 @@ display: flex; flex-direction: column; - padding-top: 15px; + padding-top: 10px; - width: 370px; - max-height: 560px; + width: 100%; + height: 100%; overflow: hidden; } + +.gd-ui-ext-notifications-panel-overlay { + width: 370px; + max-height: 560px; +} diff --git a/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanelHeader.scss b/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanelHeader.scss index 7f36080840c..37e6aa169c6 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanelHeader.scss +++ b/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanelHeader.scss @@ -12,7 +12,7 @@ &__tabs { width: 100%; - height: 28px; + height: 23px; display: flex; flex-direction: column; @@ -21,7 +21,10 @@ } &__mark-all-as-read-button { - height: 28px; + display: flex; + align-items: center; + justify-content: center; + height: 23px; white-space: nowrap; border-bottom: 1px solid var(--gd-palette-complementary-3); } diff --git a/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/NotificationsPanel.tsx b/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/NotificationsPanel.tsx index d4aa57e1c76..2bc39bb8b7a 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/NotificationsPanel.tsx +++ b/libs/sdk-ui-ext/src/notificationsPanel/NotificationsPanel/NotificationsPanel.tsx @@ -97,6 +97,11 @@ export interface INotificationsPanelProps extends INotificationsPanelCustomCompo */ locale?: ILocale; + /** + * Render notifications panel inline (without button + clicking on it). + */ + renderInline?: boolean; + /** * Handler for notification click. */ @@ -137,6 +142,7 @@ function NotificationsPanelController({ NotificationsListErrorState = DefaultNotificationsListErrorState, Notification = DefaultNotification, onNotificationClick, + renderInline = false, }: INotificationsPanelProps) { const { buttonRef, @@ -170,48 +176,75 @@ function NotificationsPanelController({ return ( <> - 0} - /> - {isOpen ? ( - - + ) : ( + <> + 0} /> - - ) : null} + {isOpen ? ( + + + + ) : null} + + )} ); } diff --git a/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.scss b/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.scss index 1ccaa849e2b..c6dbeac0002 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.scss +++ b/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.scss @@ -1,6 +1,10 @@ // (C) 2024 GoodData Corporation .gd-ui-ext-virtual-list { + display: flex; + flex-direction: column; + min-height: 0; + max-height: 100%; height: 100%; width: 100%; @@ -9,6 +13,8 @@ } &__scroll-container { + min-height: 0; + max-height: 100%; overflow: auto; overflow-x: hidden; diff --git a/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.tsx b/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.tsx index 9568ef2c06c..030fa6a85e3 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.tsx +++ b/libs/sdk-ui-ext/src/notificationsPanel/components/VirtualList.tsx @@ -7,7 +7,7 @@ import { Skeleton } from "./Skeleton.js"; import { bem } from "../bem.js"; export interface IPagedVirtualListProps { - maxHeight?: number; + maxHeight: number; items?: T[]; itemHeight: number; itemsGap: number; @@ -80,12 +80,12 @@ function useVirtualList(props: IPagedVirtualListProps) { const { items, itemHeight, - maxHeight = 500, itemsGap, skeletonItemsCount, hasNextPage, loadNextPage, isLoading, + maxHeight, } = props; const scrollContainerRef = React.useRef(null); @@ -99,12 +99,12 @@ function useVirtualList(props: IPagedVirtualListProps) { renderItemsCount = skeletonItemsCount; } - const height = Math.min( + const realHeight = itemsCount > 0 ? (itemHeight + itemsGap) * itemsCount + itemsGap - : skeletonItemsCount * (itemHeight + itemsGap) + itemsGap, - maxHeight, - ); + : skeletonItemsCount * (itemHeight + itemsGap) + itemsGap; + + const height = Math.min(maxHeight, realHeight); const hasScroll = scrollContainerRef.current ? scrollContainerRef.current?.scrollHeight > diff --git a/libs/sdk-ui-ext/src/notificationsPanel/data/useFetchNotifications.ts b/libs/sdk-ui-ext/src/notificationsPanel/data/useFetchNotifications.ts index 44e39b4fc9f..3784bc378c5 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/data/useFetchNotifications.ts +++ b/libs/sdk-ui-ext/src/notificationsPanel/data/useFetchNotifications.ts @@ -95,6 +95,14 @@ export function useFetchNotifications({ } }, [status, hasNextPage]); + const reset = useCallback(() => { + setPage(0); + setNotifications([]); + setHasNextPage(false); + setTotalNotificationsCount(0); + setInvalidationId((x) => x + 1); + }, []); + return { notifications, status, @@ -102,5 +110,6 @@ export function useFetchNotifications({ hasNextPage, loadNextPage, totalNotificationsCount, + reset, }; } diff --git a/libs/sdk-ui-ext/src/notificationsPanel/data/useNotificationFiltersDetail.tsx b/libs/sdk-ui-ext/src/notificationsPanel/data/useNotificationFiltersDetail.tsx index a897fe03b4a..a30260dd300 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/data/useNotificationFiltersDetail.tsx +++ b/libs/sdk-ui-ext/src/notificationsPanel/data/useNotificationFiltersDetail.tsx @@ -9,12 +9,17 @@ import { LocalIdRef, IAlertNotification, ObjRef, + idRef, + IInsightWidget, + IdentifierRef, + filterLocalIdentifier, } from "@gooddata/sdk-model"; import { useBackendStrict, useCancelablePromise, useWorkspaceStrict } from "@gooddata/sdk-ui"; import { useMemo } from "react"; import { translateAttributeFilter } from "./attributeFilterNaming.js"; import { defineMessages, useIntl } from "react-intl"; import { translateDateFilter } from "./dateFilterNaming.js"; +import { IAnalyticalBackend, layoutWidgets } from "@gooddata/sdk-backend-spi"; const messages = defineMessages({ title: { @@ -32,6 +37,18 @@ function getObjRefInScopeLocalId(attributeFilter: IAttributeFilter) { return (attributeFilter.negativeAttributeFilter.displayForm as LocalIdRef).localIdentifier; } +function fetchAutomation(backend: IAnalyticalBackend, workspaceId: string, automationId: string) { + return backend.workspace(workspaceId).automations().getAutomation(automationId); +} + +function fetchDashboard(backend: IAnalyticalBackend, workspaceId: string, dashboardId: string) { + return backend.workspace(workspaceId).dashboards().getDashboardWithReferences(idRef(dashboardId)); +} + +function fetchLabels(backend: IAnalyticalBackend, workspaceId: string, filterDisplayFormsRefs: ObjRef[]) { + return backend.workspace(workspaceId).attributes().getAttributeDisplayForms(filterDisplayFormsRefs); +} + export function useNotificationsFilterDetail(notification: IAlertNotification) { const workspaceId = useWorkspaceStrict(undefined, "NotificationTriggerDetails"); const backend = useBackendStrict(undefined, "NotificationTriggerDetails"); @@ -42,10 +59,8 @@ export function useNotificationsFilterDetail(notification: IAlertNotification) { if (!notification.automationId) { return null; } - const automation = await backend - .workspace(workspaceId) - .automations() - .getAutomation(notification.automationId); + + const automation = await fetchAutomation(backend, workspaceId, notification.automationId); const automationAlert = automation?.alert; if (!automationAlert) { @@ -73,12 +88,17 @@ export function useNotificationsFilterDetail(notification: IAlertNotification) { }) .filter(Boolean) as ObjRef[]; - const labels = await backend - .workspace(workspaceId) - .attributes() - .getAttributeDisplayForms(filterDisplayFormsRefs); + const dashboardId = automation?.dashboard; + + const dashboardPromise = dashboardId + ? fetchDashboard(backend, workspaceId, dashboardId) + : Promise.resolve(null); + + const labelsPromise = fetchLabels(backend, workspaceId, filterDisplayFormsRefs); + + const [dashboard, labels] = await Promise.all([dashboardPromise, labelsPromise]); - return { automation, labels }; + return { automation, dashboard, labels }; }, }, [notification.automationId, workspaceId], @@ -88,14 +108,26 @@ export function useNotificationsFilterDetail(notification: IAlertNotification) { if (!automationPromise.result) { return null; } - const { automation, labels } = automationPromise.result; + const { automation, dashboard, labels } = automationPromise.result; const alert = automation?.alert; if (!alert) { return []; } - return alert.execution.filters + const widgets = dashboard?.dashboard.layout ? layoutWidgets(dashboard.dashboard.layout) : []; + const widget = widgets.find((w) => w.identifier === automation.metadata?.widget); + const insight = dashboard?.references.insights.find( + (i) => i.insight.identifier === ((widget as IInsightWidget).insight as IdentifierRef).identifier, + ); + const filtersWithoutInsightFilters = alert.execution.filters.filter((f) => { + const insightFilter = insight?.insight.filters.find((f2) => { + return filterLocalIdentifier(f) === filterLocalIdentifier(f2); + }); + return !insightFilter; + }); + + return filtersWithoutInsightFilters .map((filter) => { let ref = filterObjRef(filter); let subtitle = ""; diff --git a/libs/sdk-ui-ext/src/notificationsPanel/data/useNotifications.tsx b/libs/sdk-ui-ext/src/notificationsPanel/data/useNotifications.tsx index 76ab6d2b93a..053f576183b 100644 --- a/libs/sdk-ui-ext/src/notificationsPanel/data/useNotifications.tsx +++ b/libs/sdk-ui-ext/src/notificationsPanel/data/useNotifications.tsx @@ -13,7 +13,7 @@ import { useFetchNotifications } from "./useFetchNotifications.js"; export interface IUseNotificationsProps { workspace?: string; backend?: IAnalyticalBackend; - refreshInterval?: number; + refreshInterval: number; } /** @@ -27,6 +27,7 @@ export function useNotifications({ workspace, refreshInterval }: IUseNotificatio error: notificationsError, loadNextPage: notificationsLoadNextPage, status: notificationsStatus, + reset: notificationsReset, } = useFetchNotifications({ workspace: effectiveWorkspace, refreshInterval, @@ -38,6 +39,7 @@ export function useNotifications({ workspace, refreshInterval }: IUseNotificatio notifications: unreadNotifications, status: unreadNotificationsStatus, totalNotificationsCount: unreadNotificationsCount, + reset: unreadNotificationsReset, } = useFetchNotifications({ workspace: effectiveWorkspace, readStatus: "unread", @@ -83,13 +85,14 @@ export function useNotifications({ workspace, refreshInterval }: IUseNotificatio await organizationService.notifications().markAllNotificationsAsRead(); - setMarkedAsReadNotifications(notifications.map((notification) => notification.id) ?? []); + notificationsReset(); + unreadNotificationsReset(); }, [ organizationService, organizationStatus, - notifications, notificationsStatus, - setMarkedAsReadNotifications, + notificationsReset, + unreadNotificationsReset, ]); const effectiveNotifications = useMemo(() => { diff --git a/libs/sdk-ui-kit/api/sdk-ui-kit.api.md b/libs/sdk-ui-kit/api/sdk-ui-kit.api.md index 7814a84e4ca..ad0df064f7d 100644 --- a/libs/sdk-ui-kit/api/sdk-ui-kit.api.md +++ b/libs/sdk-ui-kit/api/sdk-ui-kit.api.md @@ -827,7 +827,10 @@ export interface IAppHeaderProps { // (undocumented) menuItemsGroups?: IHeaderMenuItem[][]; // (undocumented) - notificationsPanel?: React_2.ReactNode; + notificationsPanel?: (props: { + isMobile: boolean; + closeNotificationsOverlay: () => void; + }) => React_2.ReactNode; // (undocumented) onChatItemClick?: (e: React_2.MouseEvent) => void; // (undocumented) @@ -867,6 +870,8 @@ export interface IAppHeaderState { // (undocumented) isHelpMenuOpen: boolean; // (undocumented) + isNotificationsMenuOpen: boolean; + // (undocumented) isOverlayMenuOpen: boolean; // (undocumented) isSearchMenuOpen: boolean; @@ -2014,6 +2019,8 @@ export interface IHeaderMenuItem { // (undocumented) href?: string; // (undocumented) + icon?: React_2.ReactNode; + // (undocumented) iconName?: string; // (undocumented) isActive?: boolean; @@ -4879,13 +4886,15 @@ export const Typography: React_2.FC; export type TypographyTagName = "h1" | "h2" | "h3" | "p"; // @internal (undocumented) -export const UiButton: ({ buttonRef, size, variant, label, isDisabled, isLoading, iconBefore, iconAfter, onClick, }: UiButtonProps) => React_2.JSX.Element; +export const UiButton: ({ buttonRef, size, variant, label, isDisabled, isLoading, iconBefore, iconAfter, onClick, dataId, }: UiButtonProps) => React_2.JSX.Element; // @internal (undocumented) export interface UiButtonProps { // (undocumented) buttonRef?: React_2.RefObject; // (undocumented) + dataId?: string; + // (undocumented) iconAfter?: IconType; // (undocumented) iconBefore?: IconType; diff --git a/libs/sdk-ui-kit/src/@ui/UiButton/UiButton.tsx b/libs/sdk-ui-kit/src/@ui/UiButton/UiButton.tsx index dceaeb67e46..2231eb17659 100644 --- a/libs/sdk-ui-kit/src/@ui/UiButton/UiButton.tsx +++ b/libs/sdk-ui-kit/src/@ui/UiButton/UiButton.tsx @@ -27,6 +27,7 @@ export interface UiButtonProps { isLoading?: boolean; tooltip?: React.ReactNode; onClick?: (e: React.MouseEvent) => void; + dataId?: string; } const { b, e } = bem("gd-ui-kit-button"); @@ -44,6 +45,7 @@ export const UiButton = ({ iconBefore, iconAfter, onClick, + dataId, }: UiButtonProps) => { const iconPosition = iconBefore ? "left" : iconAfter ? "right" : undefined; @@ -54,6 +56,7 @@ export const UiButton = ({ disabled={isDisabled} tabIndex={0} onClick={onClick} + data-id={dataId} > {iconBefore ? ( diff --git a/libs/sdk-ui-kit/src/Header/Header.tsx b/libs/sdk-ui-kit/src/Header/Header.tsx index fa72aff98bd..a890ad7b135 100644 --- a/libs/sdk-ui-kit/src/Header/Header.tsx +++ b/libs/sdk-ui-kit/src/Header/Header.tsx @@ -57,6 +57,7 @@ class AppHeaderCore extends Component = { logoHref: "/", helpMenuDropdownAlignPoints: "br tr", @@ -64,6 +65,7 @@ class AppHeaderCore extends Component(); @@ -80,6 +82,7 @@ class AppHeaderCore extends Component ({ isSearchMenuOpen: !isSearchMenuOpen, isHelpMenuOpen: false, + isNotificationsMenuOpen: false, + })); + }; + + private toggleNotificationsMenu = () => { + this.setState(({ isNotificationsMenuOpen }) => ({ + isNotificationsMenuOpen: !isNotificationsMenuOpen, + isHelpMenuOpen: false, + isSearchMenuOpen: false, + })); + }; + + private closeNotificationsMenu = () => { + this.setState(() => ({ + isNotificationsMenuOpen: false, + isHelpMenuOpen: false, + isSearchMenuOpen: false, + isOverlayMenuOpen: false, })); }; @@ -208,6 +231,7 @@ class AppHeaderCore extends Component { - if (!this.props.search) { + private addAdditionalItems = (itemGroups: IHeaderMenuItem[][]): IHeaderMenuItem[][] => { + const additionalItems = []; + if (this.props.search) { + additionalItems.push({ + key: "gs.header.search", + className: "gd-icon-header-search", + onClick: this.toggleSearchMenu, + }); + } + + if (this.props.notificationsPanel) { + additionalItems.push({ + key: "gs.header.notifications", + className: "gd-icon-header-notifications", + icon: ( + + + + ), + onClick: this.toggleNotificationsMenu, + }); + } + + if (!additionalItems.length) { return itemGroups; } - return [ - ...itemGroups, - [ - { - key: "gs.header.search", - className: "gd-icon-header-search", - onClick: this.toggleSearchMenu, - }, - ], - ]; + return [...itemGroups, additionalItems]; }; private getHelpMenu = () => [ @@ -294,6 +331,7 @@ class AppHeaderCore extends Component { + let content = this.renderVerticalMenu(); + if (this.state.isSearchMenuOpen) { + content = this.renderSearchMenu(); + } + if (this.state.isNotificationsMenuOpen) { + content = this.renderNotificationsOverlay(); + } + return ( - {this.state.isSearchMenuOpen ? this.renderSearchMenu() : this.renderVerticalMenu()} + {content} @@ -352,6 +401,23 @@ class AppHeaderCore extends Component { + if (!this.props.notificationsPanel) { + return null; + } + return ( +
+ + + + {this.props.notificationsPanel({ + isMobile: true, + closeNotificationsOverlay: this.closeNotificationsMenu, + })} +
+ ); + }; + private renderTrialItems = () => { if (this.props.expiredDate || this.props.showUpsellButton) { return ( @@ -377,7 +443,7 @@ class AppHeaderCore extends Component : null} - {this.props.notificationsPanel ?? null} + {this.props.notificationsPanel + ? this.props.notificationsPanel({ + isMobile: false, + closeNotificationsOverlay: this.closeNotificationsMenu, + }) + : null} {this.props.search ? ( void; } @@ -62,6 +63,7 @@ export const CoreHeaderHelp: React.FC = ({ className={cx("gd-list-item gd-list-help-menu-item", { [item.className]: !!item.className })} > {item.iconName ? : null} + {item.icon ? item.icon : null} diff --git a/libs/sdk-ui-kit/src/Header/HeaderMenu.tsx b/libs/sdk-ui-kit/src/Header/HeaderMenu.tsx index 5118adae25e..c73854404d0 100644 --- a/libs/sdk-ui-kit/src/Header/HeaderMenu.tsx +++ b/libs/sdk-ui-kit/src/Header/HeaderMenu.tsx @@ -1,4 +1,4 @@ -// (C) 2007-2022 GoodData Corporation +// (C) 2007-2024 GoodData Corporation import React, { PureComponent, ReactNode } from "react"; import { injectIntl, FormattedMessage, WrappedComponentProps } from "react-intl"; import { v4 as uuid } from "uuid"; @@ -38,6 +38,7 @@ class WrappedHeaderMenu extends PureComponent + {item.icon ? item.icon : null} {item.iconName ? : null} diff --git a/libs/sdk-ui-kit/src/Header/typings.ts b/libs/sdk-ui-kit/src/Header/typings.ts index 10aebeb3f93..29c78b4b3e3 100644 --- a/libs/sdk-ui-kit/src/Header/typings.ts +++ b/libs/sdk-ui-kit/src/Header/typings.ts @@ -14,6 +14,7 @@ export interface IHeaderMenuItem { className?: string; target?: string; iconName?: string; + icon?: React.ReactNode; onClick?: (obj: any) => void; } @@ -62,7 +63,10 @@ export interface IAppHeaderProps { onInviteItemClick?: (e: React.MouseEvent) => void; search?: React.ReactNode; - notificationsPanel?: React.ReactNode; + notificationsPanel?: (props: { + isMobile: boolean; + closeNotificationsOverlay: () => void; + }) => React.ReactNode; showChatItem?: boolean; onChatItemClick?: (e: React.MouseEvent) => void; } @@ -77,6 +81,7 @@ export interface IAppHeaderState { responsiveMode: boolean; isHelpMenuOpen: boolean; isSearchMenuOpen: boolean; + isNotificationsMenuOpen: boolean; } /** diff --git a/libs/sdk-ui-kit/styles/scss/header.scss b/libs/sdk-ui-kit/styles/scss/header.scss index 26cccc512f0..49a31050fc5 100644 --- a/libs/sdk-ui-kit/styles/scss/header.scss +++ b/libs/sdk-ui-kit/styles/scss/header.scss @@ -393,7 +393,8 @@ $button-normal-active-shadow: color.adjust($button-normal-active-border-color, $ align-items: center; } -.gd-header-menu-search { +.gd-header-menu-search, +.gd-header-menu-notifications { display: flex; flex-direction: column; background-color: variables.$gd-color-white; @@ -471,6 +472,70 @@ $button-normal-active-shadow: color.adjust($button-normal-active-border-color, $ } } +.gd-header-notifications-icon { + padding-right: 5px; +} + +.gd-header-notifications { + height: 100%; + margin: 0; + line-height: 42px; + opacity: 0.8; + cursor: pointer; + transition: all 0.2s; + font-size: 14px; + font-weight: 400; + position: relative; + box-sizing: border-box; + padding: 0 34px 2px 13px; + + @include mixins.text-overflow; + + &:hover, + &.is-open { + opacity: 1; + } + + &::after { + content: "\e612"; + position: absolute; + top: 0; + right: 11px; + margin-left: 11px; + width: 12px; + opacity: 0.5; + text-align: center; + font-family: variables.$gd-font-indigo; + font-size: 18px; + font-weight: 700; + } + + &.is-open::after { + content: "\e613"; + } + + &:hover { + background: rgba(255, 255, 255, 0.3); + } + + &.is-open { + background-color: rgba(255, 255, 255, 0.3); + } + + &-dropdown { + overflow: hidden; + + .gd-list { + min-width: 210px; + + .gd-list-item { + font-family: variables.$gd-font-primary; + font-weight: 400; + } + } + } +} + .gd-header-chat { height: 100%; margin: 0; @@ -787,7 +852,8 @@ $button-normal-active-shadow: color.adjust($button-normal-active-border-color, $ } } - &.search-open { + &.search-open, + &.notifications-open { color: variables.$gd-color-text; } } diff --git a/libs/sdk-ui/src/base/localization/bundles/en-US.json b/libs/sdk-ui/src/base/localization/bundles/en-US.json index d7835b4224f..99ebd42cf66 100644 --- a/libs/sdk-ui/src/base/localization/bundles/en-US.json +++ b/libs/sdk-ui/src/base/localization/bundles/en-US.json @@ -19,6 +19,11 @@ "comment": "A global semantic search button label in the main menu", "limit": 0 }, + "gs.header.notifications": { + "value": "Notifications", + "comment": "A global notifications button label in the main menu", + "limit": 0 + }, "gs.header.logout": { "value": "Logout", "comment": "",