Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove ToastPresentation usage and use toaster to display messages #3573

Merged
merged 28 commits into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d5c718d
Remove ToastPresentation usage and use toaster to display toasts
veekeys May 2, 2022
9773d3c
Deprecate messages
veekeys May 2, 2022
3586291
call `MessageManager` to render toasts
veekeys May 23, 2022
4019c72
Added custom activity message
veekeys May 26, 2022
69ad08a
deprecation
veekeys May 26, 2022
d7626e5
api changes
veekeys May 26, 2022
ab3281c
Merge branch 'master' of https://github.com/imodeljs/imodeljs into vy…
veekeys May 26, 2022
7efac6b
Merge branch 'master' of https://github.com/imodeljs/imodeljs into vy…
veekeys May 27, 2022
0bd63bc
appui-react api change
veekeys May 27, 2022
10be565
readme update
veekeys May 27, 2022
6deb58d
Merge branch 'master' of https://github.com/imodeljs/imodeljs into vy…
veekeys Jun 1, 2022
3e0cc67
Merge branch 'master' into vyki/toasts-refactor
aruniverse Jun 1, 2022
9f06413
readme fixes
veekeys Jun 2, 2022
df07b80
fix linter
veekeys Jun 2, 2022
2e4c93e
add missing tests
veekeys Jun 2, 2022
7b81c0d
api update
veekeys Jun 3, 2022
5c0ac82
Merge branch 'master' of https://github.com/imodeljs/imodeljs into vy…
veekeys Jun 28, 2022
f1139d8
Fix tests
veekeys Jul 8, 2022
495427c
Merge branch 'master' of https://github.com/imodeljs/imodeljs into vy…
veekeys Jul 8, 2022
6eda004
api changes
veekeys Jul 11, 2022
a8a35ae
Fixed tests
veekeys Jul 12, 2022
e152c15
test coverage increase
veekeys Jul 13, 2022
5cc4b16
Merge branch 'master' of https://github.com/imodeljs/imodeljs into vy…
veekeys Jul 13, 2022
146c0a2
api change
veekeys Jul 13, 2022
110e510
Add support for max sticky messages;
veekeys Jul 14, 2022
07a11cd
remove deprecation
veekeys Jul 14, 2022
69fab98
fix duplication
veekeys Jul 20, 2022
1028978
Merge branch 'master' into vyki/toasts-refactor
veekeys Jul 21, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,7 @@ export class ComponentExamplesProvider {
};

private static get messageSamples(): ComponentExampleCategory {
MessageManager.registerAnimateOutRef(null);
return {
title: "Messages",
examples: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* @module Message
*/

import { MessageSeverity } from "@itwin/appui-abstract";

/** Available status types of status message.
* @internal
*/
Expand Down Expand Up @@ -42,4 +44,26 @@ export class StatusHelpers {
return StatusHelpers.WARNING_CLASS_NAME;
}
}

public static severityToStatus(severity: MessageSeverity): Status {
let status = Status.Information;

switch (severity) {
case MessageSeverity.None:
status = Status.Success;
break;
case MessageSeverity.Information:
status = Status.Information;
break;
case MessageSeverity.Warning:
status = Status.Warning;
break;
case MessageSeverity.Error:
case MessageSeverity.Fatal:
status = Status.Error;
break;
}

return status;
}
}
129 changes: 105 additions & 24 deletions ui/appui-react/src/appui-react/messages/ActivityMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@
*/

import * as React from "react";
import { UiCore } from "@itwin/core-react";
import { MessageHyperlink, MessageLayout, MessageProgress, Status } from "@itwin/appui-layout-react";
import { Small } from "@itwin/itwinui-react";
import { ToastPresentation } from "@itwin/itwinui-react/cjs/core/Toast/Toast";
import { Icon, UiCore } from "@itwin/core-react";
import {
Message,
MessageButton,
MessageHyperlink,
MessageLayout,
MessageProgress,
Status,
} from "@itwin/appui-layout-react";
import { ProgressLinear, Small, Text, toaster } from "@itwin/itwinui-react";
import { UiFramework } from "../UiFramework";
import { ActivityMessageEventArgs } from "../messages/MessageManager";
import { ActivityMessageEventArgs, MessageManager } from "../messages/MessageManager";
import { MessageLabel } from "./MessageLabel";
import { HollowIcon } from "./HollowIcon";
import { ToasterSettings } from "@itwin/itwinui-react/cjs/core/Toast/Toaster";

/** Properties for a [[ActivityMessage]]
* @public
Expand All @@ -26,37 +34,110 @@ export interface ActivityMessageProps {

/** Activity Message React component
* @public
* @deprecated
*/
export function ActivityMessage(props: ActivityMessageProps) {
const messageDetails = props.activityMessageInfo.details;
const [percentCompleteLabel] = React.useState(UiFramework.translate("activityCenter.percentComplete"));
const [cancelLabel] = React.useState(UiCore.translate("dialog.cancel"));

return (
<ToastPresentation
category="informational"
hasCloseButton={true}
onClose={props.dismissActivityMessage}
content={
<MessageLayout
buttons={(messageDetails && messageDetails.supportsCancellation) &&
<MessageHyperlink onClick={props.cancelActivityMessage}>{cancelLabel}</MessageHyperlink>
}
progress={(messageDetails && messageDetails.showProgressBar) &&
<Message // eslint-disable-line deprecation/deprecation
veekeys marked this conversation as resolved.
Show resolved Hide resolved
status={Status.Information}
icon={<HollowIcon iconSpec="icon-info-hollow" />}
>
<MessageLayout
buttons={(messageDetails && messageDetails.supportsCancellation) ? (
<div>
<MessageHyperlink onClick={props.cancelActivityMessage}>
{cancelLabel}
</MessageHyperlink>
<span>&nbsp;</span>
<MessageButton onClick={props.dismissActivityMessage}>
<Icon iconSpec="icon-close" />
</MessageButton>
</div>
) : (
<MessageButton onClick={props.dismissActivityMessage}>
<Icon iconSpec="icon-close" />
</MessageButton>
)}
progress={(messageDetails && messageDetails.showProgressBar) &&
<MessageProgress
status={Status.Information}
progress={props.activityMessageInfo.percentage}
/>
}
>
<div>
<MessageLabel message={props.activityMessageInfo.message} className="uifw-statusbar-message-brief" />
{(messageDetails && messageDetails.showPercentInMessage) &&
<Small>{props.activityMessageInfo.percentage + percentCompleteLabel}</Small>
}
>
<div>
<MessageLabel message={props.activityMessageInfo.message} className="uifw-statusbar-message-brief" />
{(messageDetails && messageDetails.showPercentInMessage) &&
<Small>{props.activityMessageInfo.percentage + percentCompleteLabel}</Small>
}
</div>
</MessageLayout>
</div>
</MessageLayout>
</Message>
);
}

interface CustomActivityMessageProps {
cancelActivityMessage?: () => void;
dismissActivityMessage?: () => void;
activityMessageInfo?: ActivityMessageEventArgs;
settings?: ToasterSettings;
}

/**
* Hook to render an Activity message.
*/
export function useActivityMessage({activityMessageInfo, dismissActivityMessage, cancelActivityMessage, settings}: CustomActivityMessageProps) {
veekeys marked this conversation as resolved.
Show resolved Hide resolved
const [cancelLabel] = React.useState(UiCore.translate("dialog.cancel"));
const recentToast = React.useRef<{close: () => void} | undefined>();
React.useEffect(() => {
toaster.setSettings(settings ?? {placement: "top"});
}, [settings]);

React.useEffect(() => {
if (activityMessageInfo?.restored) {
recentToast.current = toaster.informational(
<CustomActivityMessageContent initialActivityMessageInfo={activityMessageInfo} />,
{ onRemove: dismissActivityMessage, type: "persisting", link: activityMessageInfo?.details?.supportsCancellation && cancelActivityMessage ? { title: cancelLabel, onClick: cancelActivityMessage } : undefined}
);
}
if (!activityMessageInfo) {
recentToast.current?.close();
}
}, [activityMessageInfo, cancelActivityMessage, cancelLabel, dismissActivityMessage]);
}

/**
* Component wrapping the `useActivityMessage` hook to use in class components.
*/
export function CustomActivityMessageRenderer({activityMessageInfo, dismissActivityMessage, cancelActivityMessage, settings}: CustomActivityMessageProps) {
veekeys marked this conversation as resolved.
Show resolved Hide resolved
useActivityMessage({activityMessageInfo, cancelActivityMessage, dismissActivityMessage, settings});

return <></>;
}

function CustomActivityMessageContent({initialActivityMessageInfo}: {initialActivityMessageInfo?: ActivityMessageEventArgs}) {
veekeys marked this conversation as resolved.
Show resolved Hide resolved
const [percentCompleteLabel] = React.useState(UiFramework.translate("activityCenter.percentComplete"));
const [activityMessageInfo, setActivityMessageInfo] = React.useState(initialActivityMessageInfo);

React.useEffect(() => {
const handleActivityMessageUpdatedEvent = (args: ActivityMessageEventArgs) => {
setActivityMessageInfo(args);
};

return MessageManager.onActivityMessageUpdatedEvent.addListener(handleActivityMessageUpdatedEvent);
}, []);

return (
<>
{activityMessageInfo?.message && <Text>{activityMessageInfo.message}</Text>}
{!!activityMessageInfo?.details?.showPercentInMessage &&
<Small>{`${activityMessageInfo.percentage} ${percentCompleteLabel}`}</Small>
}
/>
{activityMessageInfo?.details?.showProgressBar && <ProgressLinear value={activityMessageInfo?.percentage} />}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@

import "./ActivityMessagePopup.scss";
import * as React from "react";
import classnames from "classnames";
import { ActivityMessageEventArgs, MessageManager } from "../messages/MessageManager";
import { CommonProps } from "@itwin/core-react";
import { ActivityMessage } from "./ActivityMessage";
import { useActivityMessage } from "./ActivityMessage";

/** Properties for [[ActivityMessagePopup]] component
* @public
Expand All @@ -26,22 +25,18 @@ export interface ActivityMessagePopupProps extends CommonProps {
*/
export function ActivityMessagePopup(props: ActivityMessagePopupProps) {
const [activityMessageInfo, setActivityMessageInfo] = React.useState<ActivityMessageEventArgs | undefined>(undefined);
const [isActivityMessageVisible, setIsActivityMessageVisible] = React.useState(false);

React.useEffect(() => {
const handleActivityMessageUpdatedEvent = (args: ActivityMessageEventArgs) => {
setActivityMessageInfo(args);
if (args.restored)
setIsActivityMessageVisible(true);
};

return MessageManager.onActivityMessageUpdatedEvent.addListener(handleActivityMessageUpdatedEvent);
}, [isActivityMessageVisible]);
}, []);

React.useEffect(() => {
const handleActivityMessageCancelledEvent = () => {
setActivityMessageInfo(undefined);
setIsActivityMessageVisible(false);
};

return MessageManager.onActivityMessageCancelledEvent.addListener(handleActivityMessageCancelledEvent);
Expand All @@ -53,20 +48,10 @@ export function ActivityMessagePopup(props: ActivityMessagePopupProps) {
}, [props]);

const dismissActivityMessage = React.useCallback(() => {
setIsActivityMessageVisible(false);
props.dismissActivityMessage && props.dismissActivityMessage();
}, [props]);

if (!activityMessageInfo || !isActivityMessageVisible)
return null;
useActivityMessage({activityMessageInfo, cancelActivityMessage, dismissActivityMessage});

return (
<div className={classnames("uifw-centered-popup", props.className)} style={props.style}>
<ActivityMessage
activityMessageInfo={activityMessageInfo}
cancelActivityMessage={cancelActivityMessage}
dismissActivityMessage={dismissActivityMessage}
/>
</div>
);
return null;
}
52 changes: 52 additions & 0 deletions ui/appui-react/src/appui-react/messages/MessageManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { MessageSpan } from "./MessageSpan";
import { PointerMessage } from "./Pointer";
import { NotifyMessageDetailsType, NotifyMessageType } from "./ReactNotifyMessageDetails";
import { StatusMessageManager } from "./StatusMessageManager";
import { Small, toaster, ToastOptions } from "@itwin/itwinui-react";
import { ToasterSettings } from "@itwin/itwinui-react/cjs/core/Toast/Toaster";

class MessageBoxCallbacks {
constructor(
Expand Down Expand Up @@ -143,6 +145,7 @@ export class MessageManager {
private static _messages: NotifyMessageDetailsType[] = [];
private static _ongoingActivityMessage: OngoingActivityMessage = new OngoingActivityMessage();
private static _lastMessage?: NotifyMessageDetailsType;
// eslint-disable-next-line deprecation/deprecation
private static _activeMessageManager = new StatusMessageManager();

/** The MessageAddedEvent is fired when a message is added via outputMessage(). */
Expand Down Expand Up @@ -174,8 +177,12 @@ export class MessageManager {
public static get messages(): Readonly<NotifyMessageDetailsType[]> { return this._messages; }

/** Manager of active messages. */
/** @deprecated */
// eslint-disable-next-line deprecation/deprecation
public static get activeMessageManager(): StatusMessageManager { return this._activeMessageManager; }

public static animateOutToRef: HTMLElement | null;

/** Clear the message list. */
public static clearMessages(): void {
this._messages.splice(0);
Expand Down Expand Up @@ -221,6 +228,51 @@ export class MessageManager {
MessageManager.addMessage(message);
}

public static registerAnimateOutRef(el: HTMLElement | null) {
this.animateOutToRef = el;
}

/** Display a message.
* Works only with `Sticky` and `Toast` message types.
* @param message Details about the message to display.
* @param options Optionally override individual toast parameters.
* @param settings Optionally override all toasts settings (i.e. placement or order).
* @returns Object with reference to the message (i.e. to close it programmatically) if it was displayed.
*/
public static displayMessage(message: NotifyMessageDetailsType, options?: ToastOptions, settings?: ToasterSettings) {
veekeys marked this conversation as resolved.
Show resolved Hide resolved
if (message.msgType !== OutputMessageType.Sticky && message.msgType !== OutputMessageType.Toast) {
return;
}
const toastOptions: ToastOptions = {
hasCloseButton: true,
duration: message.displayTime.milliseconds,
type:
message.msgType === OutputMessageType.Sticky
? "persisting"
: "temporary",
animateOutTo: this.animateOutToRef,
...options,
};
toaster.setSettings({ placement: "bottom", order: "ascending", ...settings });
const content = <>
{message.briefMessage}
{message.detailedMessage &&
<Small>{message.detailedMessage}</Small>
}
</>;
switch (message.priority) {
case OutputMessagePriority.Warning:
return toaster.warning(content, toastOptions);
case OutputMessagePriority.Info:
return toaster.informational(content, toastOptions);
case OutputMessagePriority.Error:
case OutputMessagePriority.Fatal:
return toaster.negative(content, toastOptions);
default:
return toaster.positive(content, toastOptions);
}
}

/** Output a message and/or alert to the user.
* @param message Details about the message to output.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface StatusMessage {

/** Manager for Status messages
* @internal
* @deprecated
*/
export class StatusMessageManager {
private _messages: ReadonlyArray<StatusMessage> = [];
Expand Down
Loading