Update comments onboarding tour (#5903)

This commit is contained in:
Rishabh Saxena 2021-07-16 14:01:26 +05:30 committed by GitHub
parent 374e8aec4a
commit 8f7165b5fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 471 additions and 175 deletions

View File

@ -1,11 +1,12 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { SaveOrgLogo, SaveOrgRequest } from "api/OrgApi";
export const fetchOrg = (orgId: string) => {
export const fetchOrg = (orgId: string, skipValidation?: boolean) => {
return {
type: ReduxActionTypes.FETCH_CURRENT_ORG,
payload: {
orgId,
skipValidation,
},
};
};

View File

@ -24,6 +24,7 @@ export interface FetchAllRolesResponse extends ApiResponse {
export interface FetchOrgRequest {
orgId: string;
skipValidation?: boolean;
}
export interface FetchAllUsersRequest {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="body_1" width="32" height="32">
<g transform="matrix(1.4545455 0 0 1.4545455 0 0)">
<g transform="matrix(1 0 0 1 0 0)">
</g>
<path transform="matrix(1 0 0 1 0 0)" d="M4 8C 4 4.13401 7.13401 1 11 1C 14.866 1 18 4.13401 18 8L18 8L18 15L11 15C 7.13401 15 4 11.866 4 8z" stroke="none" fill="#FFFFFF" fill-rule="nonzero" />
<g transform="matrix(1 0 0 1 0 0)">
</g>
<path transform="matrix(1 0 0 1 0 0)" d="M4.46667 8C 4.46667 4.39174 7.39174 1.46667 11 1.46667C 14.6083 1.46667 17.5333 4.39174 17.5333 8L17.5333 8L17.5333 14.5333L11 14.5333C 7.39174 14.5333 4.46667 11.6083 4.46667 8z" stroke="#E0DEDE" stroke-width="0.933333" fill="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 895 B

View File

@ -52,6 +52,8 @@ import { Variant } from "components/ads/common";
import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
import { TourType } from "entities/Tour";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
import { commentsTourStepsEditModeTypes } from "comments/tour/commentsTourSteps";
const StyledContainer = styled.div`
width: 100%;
@ -202,18 +204,19 @@ const reduceReactions = (
(Array.isArray(reactions) &&
reactions.reduce(
(res: Record<string, ComponentReaction>, reaction: Reaction) => {
const { byUsername, emoji } = reaction;
const { byName, byUsername, emoji } = reaction;
const sameAsCurrent = byUsername === username;
const name = byName || byUsername;
if (res[reaction.emoji]) {
res[reaction.emoji].count++;
if (!sameAsCurrent) {
res[reaction.emoji].users = [
...(res[reaction.emoji].users || []),
byUsername,
name,
];
}
} else {
const users = !sameAsCurrent ? [byUsername] : [];
const users = !sameAsCurrent ? [name] : [];
res[emoji] = {
count: 1,
reactionEmoji: emoji,
@ -263,6 +266,10 @@ function CommentCard({
inline?: boolean;
visible?: boolean;
}) {
const proceedToNextTourStep = useProceedToNextTourStep({
[TourType.COMMENTS_TOUR_EDIT_MODE]: commentsTourStepsEditModeTypes.RESOLVE,
});
const [isHovered, setIsHovered] = useState(false);
const [cardMode, setCardMode] = useState(CommentCardModes.VIEW);
const dispatch = useDispatch();
@ -427,11 +434,16 @@ function CommentCard({
<ResolveButtonContainer>
{inline ? (
<TourTooltipWrapper
tourIndex={2}
tourType={TourType.COMMENTS_TOUR}
activeStepConfig={{
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.RESOLVE,
}}
>
<ResolveCommentButton
handleClick={toggleResolved as () => void}
handleClick={() => {
toggleResolved && toggleResolved();
proceedToNextTourStep();
}}
resolved={!!resolved}
/>
</TourTooltipWrapper>

View File

@ -42,7 +42,7 @@ const ThreadContainer = styled(animated.div)<{
max-height: ${(props) =>
props.inline ? `calc(100vh - ${props.theme.smallHeaderHeight})` : "unset"};
/* overflow: auto collapses the comment threads in the sidebar */
overflow: ${(props) => (props.inline ? "auto" : "unset")};
overflow: ${(props) => (props.inline ? "visible" : "unset")};
`;
const CommentsContainer = styled.div<{ inline?: boolean }>`

View File

@ -23,40 +23,50 @@ import { updateUserDetails } from "actions/userActions";
import { S3_BUCKET_URL } from "constants/ThirdPartyConstants";
import {
isPermitted,
PERMISSION_TYPE,
} from "pages/Applications/permissionHelpers";
import { getCurrentAppOrg } from "selectors/organizationSelectors";
import { Org } from "constants/orgConstants";
import useOrg from "utils/hooks/useOrg";
const getBanner = (step: number) =>
`${S3_BUCKET_URL}/comments/step-${step}.png`;
const introSteps = [
const introStepsEditor = [
{
title: "Introducing Live Comments",
content:
"We are introducing live comments. From now on you will be able to comment on your apps, tag other people and exchange thoughts in threads. Click Next to learn more about comments and start commenting.",
"You can now collaborate with your users to build apps faster. Invite your team to comment on your apps, exchange thoughts & ship your ideas.",
banner: getBanner(1),
hideBackBtn: true,
bannerProps: { style: { height: 284 } },
},
{
title: "Give feedback",
title: "Give Contextual Feedback",
content:
"Comment on your co-workers work and share your thoughts on what works and what needs change.",
"Drop a comment on a widget to suggest an improvement. Comments are tagged to the widget and move along with it. Update the widget and iterate your way to shipping your ideas!",
banner: getBanner(2),
bannerProps: { style: { height: 284 } },
},
];
const introStepsViewer = [
{
title: "Introducing Live Comments",
content:
"You can now collaborate with your developers to build apps faster. Exchange thoughts, leave feedback & ship your ideas.",
banner: getBanner(1),
hideBackBtn: true,
bannerProps: { style: { height: 284 } },
},
{
title: "Invite other people to your conversations",
title: "Give Contextual Feedback",
content:
"When leaving a comment you can tag other people by writing @ and their name. This way the person you tagged will get a notification and an e-mail that you tagged them in a comment.",
banner: getBanner(3),
},
{
title: "Tag a comment to a widget",
content:
"If you click on a component while in a comment mode you will tag that comment to that widget. This way if the widget is moved the comment will be moved as well. You can disconnect the comment and widget y simply moving the the comment away from the widget.",
banner: getBanner(4),
},
{
title: "You are all set!",
content:
"By clicking on the comments icon in the top right corner you will activate the collaboration mode and will be able to start a thread or answer to someone elses comment.",
banner: getBanner(5),
"Drop a comment on a widget to suggest an improvement or report an issue. Comments are tagged to the widget, resolve them once the updates are live!",
banner: getBanner(2),
bannerProps: { style: { height: 284 } },
},
];
@ -74,10 +84,11 @@ function IntroStep(props: {
content: string;
banner: string;
theme: Theme;
bannerProps: any;
}) {
return (
<>
<StyledImg alt="" src={props.banner} />
<StyledImg alt="" src={props.banner} {...props.bannerProps} />
<IntroContentContainer>
<div style={{ marginBottom: props.theme.spaces[4] }}>
<Text
@ -108,30 +119,40 @@ const getSteps = (
startTutorial: () => void,
initialProfileFormValues: { emailAddress?: string; displayName?: string },
emailDisabled: boolean,
) => [
...introSteps.slice(0, 4).map((stepConfig: any) => ({
props: stepConfig,
component: IntroStepThemed,
})),
{
component: ProfileForm,
props: {
isSubmitDisabled: isSubmitProfileFormDisabled,
onSubmit: onSubmitProfileForm,
initialValues: initialProfileFormValues,
emailDisabled,
showEditorSteps: boolean,
) => {
const introSteps = showEditorSteps ? introStepsEditor : introStepsViewer;
return [
...introSteps.map((stepConfig: any) => ({
props: stepConfig,
component: IntroStepThemed,
})),
{
component: ProfileForm,
props: {
isSubmitDisabled: isSubmitProfileFormDisabled,
initialValues: initialProfileFormValues,
emailDisabled,
nextBtnText: "Start Tutorial",
onSubmit: () => {
startTutorial();
onSubmitProfileForm();
},
hideBackBtn: true,
},
},
},
{
component: IntroStepThemed,
props: {
...introSteps[4],
hideBackBtn: true,
nextBtnText: "Start Tutorial",
onSubmit: startTutorial,
},
},
];
];
};
const getCanManage = (currentOrg: Org) => {
const userOrgPermissions = currentOrg.userPermissions || [];
const canManage = isPermitted(
userOrgPermissions,
PERMISSION_TYPE.MANAGE_ORGANIZATION,
);
return canManage;
};
export default function CommentsShowcaseCarousel() {
const dispatch = useDispatch();
@ -145,15 +166,24 @@ export default function CommentsShowcaseCarousel() {
const initialProfileFormValues = { emailAddress: email, displayName: name };
const onSubmitProfileForm = () => {
const { displayName: name, emailAddress: email } = profileFormValues as {
displayName: string;
emailAddress: string;
};
const { displayName: name, emailAddress: email } =
(profileFormValues as {
displayName: string;
emailAddress: string;
}) || {};
dispatch(updateUserDetails({ name, email }));
};
const { id } = useSelector(getCurrentAppOrg) || {};
const currentOrg = useOrg(id);
const canManage = getCanManage(currentOrg);
const startTutorial = () => {
dispatch(setActiveTour(TourType.COMMENTS_TOUR));
const tourType = canManage
? TourType.COMMENTS_TOUR_EDIT_MODE
: TourType.COMMENTS_TOUR_PUBLISHED_MODE;
dispatch(setActiveTour(tourType));
dispatch(hideCommentsIntroCarousel());
setCommentsIntroSeen(true);
};
@ -164,9 +194,10 @@ export default function CommentsShowcaseCarousel() {
startTutorial,
initialProfileFormValues,
!!email,
canManage,
);
if (!isIntroCarouselVisible) return null;
if (steps.length === 0 || !isIntroCarouselVisible) return null;
return (
<CommentsCarouselModal>

View File

@ -34,6 +34,13 @@ import { change } from "redux-form";
import { INVITE_USERS_TO_ORG_FORM } from "constants/forms";
import { isEmail } from "utils/formhelpers";
import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
import { TourType } from "entities/Tour";
import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
import {
commentsTourStepsEditModeTypes,
commentsTourStepsPublishedModeTypes,
} from "comments/tour/commentsTourSteps";
const StyledInputContainer = styled.div`
width: 100%;
@ -62,8 +69,6 @@ const Row = styled.div`
justify-content: space-between;
`;
// Trigger tests
const insertCharacter = (
characterToInsert: string,
editorState: EditorState,
@ -136,14 +141,26 @@ function AddCommentInput({
onSave: (state: RawDraftContentState) => void;
onCancel?: () => void;
}) {
const proceedToNextTourStep = useProceedToNextTourStep({
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.SAY_HELLO,
[TourType.COMMENTS_TOUR_PUBLISHED_MODE]:
commentsTourStepsPublishedModeTypes.SAY_HELLO,
});
const dispatch = useDispatch();
const users = useOrgUsers();
const [suggestions, setSuggestions] = useState<Array<MentionData>>([]);
const [trigger, setTrigger] = useState<Trigger>();
useUserSuggestions(users, setSuggestions);
const [editorState, setEditorState] = useState(
const [editorState, setEditorStateInState] = useState(
initialEditorState || EditorState.createEmpty(),
);
const setEditorState = useCallback((editorState: EditorState) => {
setEditorStateInState(editorState);
}, []);
const [suggestionsQuery, setSuggestionsQuery] = useState("");
const clearEditor = useCallback(() => {
@ -166,6 +183,7 @@ function AddCommentInput({
const rawContent = convertToRaw(contentState);
clearEditor();
onSave(rawContent);
proceedToNextTourStep();
},
[editorState],
);
@ -216,44 +234,53 @@ function AddCommentInput({
};
return (
<PaddingContainer removePadding={removePadding}>
<Row>
<StyledInputContainer>
<MentionsInput
autoFocus
editorState={editorState}
onAddMention={onAddMention}
onSearchSuggestions={onSearchChange}
onSubmit={onSaveComment}
placeholder={createMessage(ADD_COMMENT_PLACEHOLDER)}
setEditorState={setEditorState}
suggestions={filteredSuggestions}
/>
</StyledInputContainer>
</Row>
<Row>
<StyledEmojiTrigger>
<EmojiPicker onSelectEmoji={handleEmojiClick} />
</StyledEmojiTrigger>
<TourTooltipWrapper
activeStepConfig={{
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.SAY_HELLO,
[TourType.COMMENTS_TOUR_PUBLISHED_MODE]:
commentsTourStepsPublishedModeTypes.SAY_HELLO,
}}
>
<PaddingContainer removePadding={removePadding}>
<Row>
<Button
category={Category.tertiary}
className={"cancel-button"}
onClick={_onCancel}
text={createMessage(CANCEL)}
type="button"
/>
<Button
category={Category.primary}
data-cy="add-comment-submit"
disabled={!editorState.getCurrentContent().hasText()}
onClick={handleSubmit}
text={createMessage(POST)}
type="button"
/>
<StyledInputContainer>
<MentionsInput
autoFocus
editorState={editorState}
onAddMention={onAddMention}
onSearchSuggestions={onSearchChange}
onSubmit={onSaveComment}
placeholder={createMessage(ADD_COMMENT_PLACEHOLDER)}
setEditorState={setEditorState}
suggestions={filteredSuggestions}
/>
</StyledInputContainer>
</Row>
</Row>
</PaddingContainer>
<Row>
<StyledEmojiTrigger>
<EmojiPicker onSelectEmoji={handleEmojiClick} />
</StyledEmojiTrigger>
<Row>
<Button
category={Category.tertiary}
className={"cancel-button"}
onClick={_onCancel}
text={createMessage(CANCEL)}
type="button"
/>
<Button
category={Category.primary}
data-cy="add-comment-submit"
disabled={!editorState.getCurrentContent().hasText()}
onClick={handleSubmit}
text={createMessage(POST)}
type="button"
/>
</Row>
</Row>
</PaddingContainer>
</TourTooltipWrapper>
);
}

View File

@ -4,14 +4,20 @@ import { useSelector, useDispatch } from "react-redux";
import Comments from "./Comments";
import { commentModeSelector } from "selectors/commentsSelectors";
import { createUnpublishedCommentThreadRequest } from "actions/commentActions";
import commentIcon from "assets/icons/comments/commentCursor.png";
import commentIcon from "assets/icons/comments/commentCursor.svg";
import { getOffsetPos } from "comments/utils";
import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
import { TourType } from "entities/Tour";
import { WidgetType } from "constants/WidgetConstants";
import {
commentsTourStepsEditModeTypes,
commentsTourStepsPublishedModeTypes,
} from "comments/tour/commentsTourSteps";
type Props = {
children: React.ReactNode;
refId: string;
widgetType: WidgetType;
};
const Container = styled.div`
@ -25,15 +31,17 @@ const Container = styled.div`
* 1. Renders inline comment threads down the tree
* 2. Calculates pin offset while creating a new comment
*/
function OverlayCommentsWrapper({ children, refId }: Props) {
function OverlayCommentsWrapper({ children, refId, widgetType }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const isCommentMode = useSelector(commentModeSelector);
const dispatch = useDispatch();
const proceedToNextTourStep = useProceedToNextTourStep(
TourType.COMMENTS_TOUR,
1,
);
const proceedToNextTourStep = useProceedToNextTourStep({
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.CREATE_UNPUBLISHED_COMMENT,
[TourType.COMMENTS_TOUR_PUBLISHED_MODE]:
commentsTourStepsPublishedModeTypes.CREATE_UNPUBLISHED_COMMENT,
});
// create new unpublished thread
const clickHandler = (e: any) => {
@ -45,6 +53,7 @@ function OverlayCommentsWrapper({ children, refId }: Props) {
dispatch(
createUnpublishedCommentThreadRequest({
refId,
widgetType,
position,
}),
);

View File

@ -1,14 +1,37 @@
import { Org } from "constants/orgConstants";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import {
isPermitted,
PERMISSION_TYPE,
} from "pages/Applications/permissionHelpers";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getAllUsers, getCurrentOrgId } from "selectors/organizationSelectors";
import {
getAllUsers,
getCurrentAppOrg,
getCurrentOrgId,
} from "selectors/organizationSelectors";
import useOrg from "utils/hooks/useOrg";
const getCanManage = (currentOrg: Org) => {
const userOrgPermissions = currentOrg.userPermissions || [];
const canManage = isPermitted(
userOrgPermissions,
PERMISSION_TYPE.MANAGE_ORGANIZATION,
);
return canManage;
};
const useOrgUsers = () => {
const dispatch = useDispatch();
const orgId = useSelector(getCurrentOrgId);
const orgUsers = useSelector(getAllUsers);
const { id } = useSelector(getCurrentAppOrg) || {};
const currentOrg = useOrg(id);
const canManage = getCanManage(currentOrg);
useEffect(() => {
if (!orgUsers || !orgUsers.length) {
if ((!orgUsers || !orgUsers.length) && canManage) {
dispatch({
type: ReduxActionTypes.FETCH_ALL_USERS_INIT,
payload: {

View File

@ -2,6 +2,10 @@ import React from "react";
import styled from "styled-components";
import TourTooltipWrapper from "components/ads/tour/TourTooltipWrapper";
import { TourType } from "entities/Tour";
import {
commentsTourStepsEditModeTypes,
commentsTourStepsPublishedModeTypes,
} from "./commentsTourSteps";
const Dot = styled.div`
position: fixed;
@ -14,7 +18,14 @@ const Dot = styled.div`
export default function AddCommentTourComponent() {
return (
<Dot>
<TourTooltipWrapper tourIndex={1} tourType={TourType.COMMENTS_TOUR}>
<TourTooltipWrapper
activeStepConfig={{
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.CREATE_UNPUBLISHED_COMMENT,
[TourType.COMMENTS_TOUR_PUBLISHED_MODE]:
commentsTourStepsPublishedModeTypes.CREATE_UNPUBLISHED_COMMENT,
}}
>
<div />
</TourTooltipWrapper>
</Dot>

View File

@ -1,11 +1,55 @@
const steps = [
import { Position } from "@blueprintjs/core";
export enum commentsTourStepsEditModeTypes {
ENTER_COMMENTS_MODE,
CREATE_UNPUBLISHED_COMMENT,
SAY_HELLO,
RESOLVE,
}
export const commentsTourStepsEditMode = [
{
id: "ENTER_COMMENTS_MODE",
data: { message: "Click on the comment icon \n to enter comments mode" },
id: commentsTourStepsEditModeTypes.ENTER_COMMENTS_MODE,
data: { message: "Click on the icon to access \n comments." },
},
{
id: "CREATE_UNPUBLISHED_COMMENT",
data: { message: "Click anywhere on the canvas \n and leave a comment." },
id: commentsTourStepsEditModeTypes.CREATE_UNPUBLISHED_COMMENT,
data: { message: "Click anywhere on the canvas \n to leave a comment." },
},
{
id: commentsTourStepsEditModeTypes.SAY_HELLO,
data: {
message: "Say hello to team appsmith",
tooltipProps: { position: Position.TOP },
},
},
{
id: commentsTourStepsEditModeTypes.RESOLVE,
data: { message: "Resolve this comment \n when you're done" },
},
];
export enum commentsTourStepsPublishedModeTypes {
ENTER_COMMENTS_MODE,
CREATE_UNPUBLISHED_COMMENT,
SAY_HELLO,
}
export const commentsTourStepsPublishedMode = [
{
id: commentsTourStepsPublishedModeTypes.ENTER_COMMENTS_MODE,
data: { message: "Click on the icon to access \n comments." },
},
{
id: commentsTourStepsPublishedModeTypes.CREATE_UNPUBLISHED_COMMENT,
data: { message: "Click anywhere on the canvas \n to leave a comment." },
},
{
id: commentsTourStepsPublishedModeTypes.SAY_HELLO,
data: {
message:
"Tag your teammate and leave them \n some suggestions for the application!",
tooltipProps: { position: Position.TOP },
},
},
];
export default steps;

View File

@ -17,12 +17,11 @@ import { Indices } from "constants/Layers";
type Props = {
children: React.ReactNode;
hasOverlay?: boolean;
tourType: TourType;
tourIndex: number;
modifiers?: Modifiers;
onClick?: () => void;
pulseStyles?: CSSProperties;
showPulse?: boolean;
activeStepConfig: { [key in TourType]?: any };
};
const Overlay = styled.div`
@ -47,16 +46,18 @@ const Container = styled.div`
`;
function TourTooltipWrapper(props: Props) {
const { children, tourIndex, tourType } = props;
const { activeStepConfig, children } = props;
const activeTourType = useSelector(getActiveTourType) as TourType;
const expectedActiveStep = activeStepConfig[activeTourType];
const isCurrentStepActive = useSelector(
(state: AppState) => getActiveTourIndex(state) === tourIndex,
(state: AppState) => getActiveTourIndex(state) === expectedActiveStep,
);
const isCurrentTourActive = useSelector(
(state: AppState) => getActiveTourType(state) === tourType,
);
const tourStepsConfig = TourStepsByType[tourType as TourType];
const tourStepConfig = tourStepsConfig[tourIndex];
const isOpen = isCurrentStepActive && isCurrentTourActive;
const tourStepsConfig = TourStepsByType[activeTourType as TourType];
const tourStepConfig = tourStepsConfig && tourStepsConfig[expectedActiveStep];
const isOpen = isCurrentStepActive;
const dotRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -78,9 +79,6 @@ function TourTooltipWrapper(props: Props) {
{/* A crude overlay which won't work with containers having overflow hidden */}
{isOpen && props.hasOverlay && <Overlay />}
<Container onClick={props.onClick ? props.onClick : noop}>
{isOpen && props.showPulse && (
<PulseDot ref={dotRef} style={props.pulseStyles} />
)}
<TooltipComponent
boundary={"viewport"}
content={
@ -99,9 +97,13 @@ function TourTooltipWrapper(props: Props) {
isOpen={!!isOpen}
modifiers={props.modifiers}
position={Position.BOTTOM}
{...tourStepConfig?.data.tooltipProps}
>
{children}
</TooltipComponent>
{isOpen && props.showPulse && (
<PulseDot ref={dotRef} style={props.pulseStyles} />
)}
</Container>
</>
);

View File

@ -1,8 +1,12 @@
import { TourType } from "entities/Tour";
import commentsTourSteps from "comments/tour/commentsTourSteps";
import {
commentsTourStepsEditMode,
commentsTourStepsPublishedMode,
} from "comments/tour/commentsTourSteps";
const TourStepsByType = {
[TourType.COMMENTS_TOUR]: commentsTourSteps,
[TourType.COMMENTS_TOUR_EDIT_MODE]: commentsTourStepsEditMode,
[TourType.COMMENTS_TOUR_PUBLISHED_MODE]: commentsTourStepsPublishedMode,
};
export default TourStepsByType;

View File

@ -321,7 +321,7 @@ export const MARK_ALL_AS_READ = () => "Mark all as read";
export const INVITE_A_NEW_USER = () => "Invite a new user";
export const REMOVE = () => "Remove";
export const NO_NOTIFICATIONS_TO_SHOW = () => "No notifications to show";
export const ONE_UNREAD_MESSAGE = () => "1 unread conversation";
export const UNREAD_MESSAGE = () => "Unread conversation";
// Showcase Carousel
export const NEXT = () => "NEXT";

View File

@ -1,4 +1,5 @@
import { COMMENT_EVENTS } from "constants/CommentConstants";
import { WidgetType } from "constants/WidgetConstants";
import { RawDraftContentState } from "draft-js";
import { APP_MODE } from "reducers/entityReducers/appReducer";
@ -40,6 +41,7 @@ export type CreateCommentThreadRequest = {
};
isViewed?: boolean;
mode?: APP_MODE;
widgetType?: WidgetType;
};
export type Reaction = {

View File

@ -1,3 +1,4 @@
export enum TourType {
COMMENTS_TOUR = "COMMENTS_TOUR",
COMMENTS_TOUR_EDIT_MODE = "COMMENTS_TOUR_EDIT_MODE",
COMMENTS_TOUR_PUBLISHED_MODE = "COMMENTS_TOUR_PUBLISHED_MODE",
}

View File

@ -78,8 +78,8 @@ const UnreadIndicator = styled.div`
const getModeFromUserRole = async (orgId: string) => {
try {
const response = await OrgApi.fetchOrg({ orgId });
const userOrgPermissions = response?.data?.data?.userPermissions || [];
const response = (await OrgApi.fetchOrg({ orgId })) as any;
const userOrgPermissions = response?.data?.userPermissions || [];
const canPublish = isPermitted(
userOrgPermissions,
PERMISSION_TYPE.PUBLISH_APPLICATION,

View File

@ -47,15 +47,6 @@ const StyledHeader = styled.div`
${(props) => getTypographyByKey(props, "p1")};
color: ${(props) => props.theme.colors.notifications.listHeaderTitle};
}
& .mark-all-as-read {
border-color: ${(props) =>
props.theme.colors.notifications.markAllAsReadButtonBackground};
background-color: ${(props) =>
props.theme.colors.notifications.markAllAsReadButtonBackground};
color: ${(props) =>
props.theme.colors.notifications.markAllAsReadButtonText};
}
`;
const Label = styled.div`
@ -81,7 +72,7 @@ function EmptyNotificationsState() {
);
}
function NotificationsListHeader() {
function NotificationsListHeader(props: { markAllAsReadDisabled: boolean }) {
const dispatch = useDispatch();
return (
@ -90,6 +81,7 @@ function NotificationsListHeader() {
<Button
category={Category.primary}
className={"mark-all-as-read"}
disabled={props.markAllAsReadDisabled}
onClick={() => {
dispatch(markAllNotificationsAsReadRequest());
}}
@ -111,11 +103,11 @@ function NotificationsList() {
const dispatch = useDispatch();
const notifications = useSelector(notificationsSelector);
const fetchingNotifications = useSelector(fetchingNotificationsSelector);
const height = Math.min(4, notifications.length) * NOTIFICATION_HEIGHT;
const height = Math.min(3.5, notifications.length) * NOTIFICATION_HEIGHT;
return (
<Container>
<NotificationsListHeader />
<NotificationsListHeader markAllAsReadDisabled={!notifications.length} />
{notifications.length > 0 ? (
<Virtuoso
components={{

View File

@ -24,18 +24,32 @@ import { useLocation } from "react-router";
import history from "utils/history";
import { Position } from "@blueprintjs/core/lib/esm/common/position";
import { TourType } from "entities/Tour";
import useProceedToNextTourStep from "utils/hooks/useProceedToNextTourStep";
import useProceedToNextTourStep, {
useIsTourStepActive,
} from "utils/hooks/useProceedToNextTourStep";
import { getCommentsIntroSeen } from "utils/storage";
import { User } from "constants/userConstants";
import { ANONYMOUS_USERNAME, User } from "constants/userConstants";
import { AppState } from "reducers";
import { APP_MODE } from "reducers/entityReducers/appReducer";
import { matchBuilderPath, matchViewerPath } from "constants/routes";
import {
AUTH_LOGIN_URL,
matchBuilderPath,
matchViewerPath,
} from "constants/routes";
import { createMessage, ONE_UNREAD_MESSAGE } from "constants/messages";
import { createMessage, UNREAD_MESSAGE } from "constants/messages";
import localStorage from "utils/localStorage";
import { getAppMode } from "selectors/applicationSelectors";
import { noop } from "lodash";
import {
commentsTourStepsEditModeTypes,
commentsTourStepsPublishedModeTypes,
} from "comments/tour/commentsTourSteps";
const getShowCommentsButtonToolTip = () => {
const flag = localStorage.getItem("ShowCommentsButtonToolTip");
return flag === null || !!flag;
@ -115,11 +129,25 @@ const useUpdateCommentMode = async (currentUser?: User) => {
);
const handleLocationUpdate = async () => {
if (!currentUser) return;
const searchParams = new URL(window.location.href).searchParams;
const isCommentMode = searchParams.get("isCommentMode");
const isCommentsIntroSeen = await getCommentsIntroSeen();
const updatedIsCommentMode = isCommentMode === "true" ? true : false;
const notLoggedId = currentUser?.username === ANONYMOUS_USERNAME;
if (notLoggedId && updatedIsCommentMode) {
const currentUrl = window.location.href;
const path = `${AUTH_LOGIN_URL}?redirectUrl=${encodeURIComponent(
currentUrl,
)}`;
history.push(path);
return;
}
if (updatedIsCommentMode && !isCommentsIntroSeen) {
dispatch(showCommentsIntroCarousel());
} else {
@ -132,7 +160,7 @@ const useUpdateCommentMode = async (currentUser?: User) => {
if (window.location.href) {
handleLocationUpdate();
}
}, [location]);
}, [location, !!currentUser]);
// fetch applications comments when comment mode is turned on
useEffect(() => {
@ -216,8 +244,12 @@ const tourToolTipProps = {
width: 30,
},
showPulse: true,
tourIndex: 0,
tourType: TourType.COMMENTS_TOUR,
activeStepConfig: {
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.ENTER_COMMENTS_MODE,
[TourType.COMMENTS_TOUR_PUBLISHED_MODE]:
commentsTourStepsPublishedModeTypes.ENTER_COMMENTS_MODE,
},
};
function ViewOrEditMode({ mode }: { mode?: APP_MODE }) {
@ -261,6 +293,44 @@ function CommentModeBtn({
);
}
const useShowCommentDiscoveryTooltip = (): [boolean, typeof noop] => {
const currentUser = useSelector(getCurrentUser);
const appMode = useSelector(getAppMode);
const initShowCommentButtonDiscoveryTooltip =
getShowCommentsButtonToolTip() &&
appMode === APP_MODE.PUBLISHED &&
currentUser?.username !== ANONYMOUS_USERNAME;
const [
showCommentButtonDiscoveryTooltip,
setShowCommentButtonDiscoveryTooltipInState,
] = useState(initShowCommentButtonDiscoveryTooltip);
useEffect(() => {
setShowCommentButtonDiscoveryTooltipInState(
initShowCommentButtonDiscoveryTooltip,
);
}, [appMode, currentUser]);
return [
showCommentButtonDiscoveryTooltip,
setShowCommentButtonDiscoveryTooltipInState,
];
};
const useShouldHide = () => {
const [shouldHide, setShouldHide] = useState(false);
const location = useLocation();
useEffect(() => {
const pathName = window.location.pathname;
const shouldShow = matchBuilderPath(pathName) || matchViewerPath(pathName);
setShouldHide(!shouldShow);
}, [location]);
return shouldHide;
};
type ToggleCommentModeButtonProps = {
showSelectedMode?: boolean;
};
@ -271,20 +341,28 @@ function ToggleCommentModeButton({
const commentsEnabled = useSelector(areCommentsEnabledForUserAndAppSelector);
const isCommentMode = useSelector(commentModeSelector);
const currentUser = useSelector(getCurrentUser);
const [
showCommentButtonDiscoveryTooltip,
setShowCommentButtonDiscoveryTooltipInState,
] = useState(getShowCommentsButtonToolTip());
] = useShowCommentDiscoveryTooltip();
const showUnreadIndicator =
useSelector(showUnreadIndicatorSelector) ||
showCommentButtonDiscoveryTooltip;
useUpdateCommentMode(currentUser);
const proceedToNextTourStep = useProceedToNextTourStep(
TourType.COMMENTS_TOUR,
0,
);
const activeStepConfig = {
[TourType.COMMENTS_TOUR_EDIT_MODE]:
commentsTourStepsEditModeTypes.ENTER_COMMENTS_MODE,
[TourType.COMMENTS_TOUR_PUBLISHED_MODE]:
commentsTourStepsPublishedModeTypes.ENTER_COMMENTS_MODE,
};
const proceedToNextTourStep = useProceedToNextTourStep(activeStepConfig);
const isTourStepActive = useIsTourStepActive(activeStepConfig);
const mode = useSelector((state: AppState) => state.entities.app.mode);
@ -296,16 +374,9 @@ function ToggleCommentModeButton({
}, [proceedToNextTourStep, setShowCommentButtonDiscoveryTooltipInState]);
// Show comment mode button only on the canvas editor and viewer
const [shouldHide, setShouldHide] = useState(false);
const location = useLocation();
useEffect(() => {
const pathName = window.location.pathname;
const shouldShow = matchBuilderPath(pathName) || matchViewerPath(pathName);
setShouldHide(!shouldShow);
}, [location]);
if (shouldHide) return null;
const shouldHide = useShouldHide();
if (!commentsEnabled) return null;
if (!commentsEnabled || shouldHide) return null;
return (
<Container>
@ -320,13 +391,13 @@ function ToggleCommentModeButton({
<ViewOrEditMode mode={mode} />
</ModeButton>
<TooltipComponent
content={createMessage(ONE_UNREAD_MESSAGE)}
content={createMessage(UNREAD_MESSAGE)}
isOpen={showCommentButtonDiscoveryTooltip}
>
<CommentModeBtn
{...{
handleSetCommentModeButton,
isCommentMode,
isCommentMode: isCommentMode || isTourStepActive, // Highlight the button during the tour
showUnreadIndicator,
showSelectedMode,
}}

View File

@ -37,6 +37,16 @@ const handleNewCommentThreadEvent = (
const showUnreadIndicator = !state.isCommentMode;
/**
* Private threads are a part of the comments onboarding
* These are termed bot threads, triggered from the backend based on the onboarding flow
* We set these as visible and hide the currently visible thread, following the create event
*/
if (thread.isPrivate) {
state.visibleCommentThreadId = thread._id;
}
return {
...state,
showUnreadIndicator,

View File

@ -60,11 +60,12 @@ export function* fetchOrgSaga(action: ReduxAction<FetchOrgRequest>) {
try {
const request: FetchOrgRequest = action.payload;
const response: FetchOrgResponse = yield call(OrgApi.fetchOrg, request);
const isValidResponse = yield validateResponse(response);
const isValidResponse = yield request.skipValidation ||
validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_ORG_SUCCESS,
payload: response.data,
payload: response.data || {},
});
}
} catch (error) {

View File

@ -358,3 +358,5 @@ export const getAllPageWidgets = createSelector(
);
},
);
export const getAppMode = (state: AppState) => state.entities.app.mode;

View File

@ -0,0 +1,25 @@
import { fetchOrg } from "actions/orgActions";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getCurrentUser } from "selectors/usersSelectors";
import { getCurrentAppOrg } from "selectors/organizationSelectors";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
const useOrg = (orgId: string) => {
const dispatch = useDispatch();
const org = useSelector(getCurrentAppOrg);
const currentUser = useSelector(getCurrentUser);
useEffect(() => {
if (!currentUser || currentUser.username === ANONYMOUS_USERNAME) return;
if ((!org || !org.userPermissions) && orgId) {
dispatch(fetchOrg(orgId, true));
}
}, [orgId, currentUser && currentUser.username]);
return org;
};
export default useOrg;

View File

@ -4,17 +4,28 @@ import { AppState } from "reducers";
import { getActiveTourIndex, getActiveTourType } from "selectors/tourSelectors";
import { proceedToNextTourStep } from "actions/tourActions";
const useProceedToNextTourStep = (tourType: TourType, tourIndex: number) => {
const dispatch = useDispatch();
export const useIsTourStepActive = (
activeTourStepConfig: { [key in TourType]?: any },
) => {
const activeTourType = useSelector(getActiveTourType);
const expectedActiveStep =
activeTourType &&
activeTourStepConfig &&
activeTourStepConfig[activeTourType];
const isCurrentStepActive = useSelector(
(state: AppState) => getActiveTourIndex(state) === tourIndex,
);
const isCurrentTourActive = useSelector(
(state: AppState) => getActiveTourType(state) === tourType,
(state: AppState) => getActiveTourIndex(state) === expectedActiveStep,
);
const isActive = isCurrentStepActive && isCurrentTourActive;
return isCurrentStepActive;
};
const useProceedToNextTourStep = (
activeTourStepConfig: { [key in TourType]?: any },
) => {
const dispatch = useDispatch();
const isActive = useIsTourStepActive(activeTourStepConfig);
return () => isActive && dispatch(proceedToNextTourStep());
};

View File

@ -276,7 +276,10 @@ abstract class BaseWidget<
*/
addOverlayComments(content: ReactNode) {
return (
<OverlayCommentsWrapper refId={this.props.widgetId}>
<OverlayCommentsWrapper
refId={this.props.widgetId}
widgetType={this.props.type}
>
{content}
</OverlayCommentsWrapper>
);