feat: callouts for wip features (#8080)

This commit is contained in:
Rishabh Saxena 2021-10-04 13:31:46 +05:30 committed by GitHub
parent bacf29848b
commit a69c8b7484
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 338 additions and 113 deletions

View File

@ -16,7 +16,7 @@ describe("Unique react keys", function() {
cy.dragAndDropToCanvas("chartwidget", { x: 200, y: 200 }); cy.dragAndDropToCanvas("chartwidget", { x: 200, y: 200 });
cy.dragAndDropToCanvas("dropdownwidget", { x: 200, y: 600 }); cy.dragAndDropToCanvas("dropdownwidget", { x: 200, y: 600 });
cy.dragAndDropToCanvas("dropdownwidget", { x: 200, y: 800 }); cy.dragAndDropToCanvas("dropdownwidget", { x: 200, y: 700 });
cy.openPropertyPane("chartwidget"); cy.openPropertyPane("chartwidget");
cy.deleteWidget(widgetsPage.chartWidget); cy.deleteWidget(widgetsPage.chartWidget);

View File

@ -60,3 +60,8 @@ export const collabUnsetEditorsPointersData = (payload: any) => ({
export const collabResetEditorsPointersData = () => ({ export const collabResetEditorsPointersData = () => ({
type: ReduxActionTypes.APP_COLLAB_RESET_EDITORS_POINTER_DATA, type: ReduxActionTypes.APP_COLLAB_RESET_EDITORS_POINTER_DATA,
}); });
export const collabConcurrentPageEditorsData = (payload: any) => ({
type: ReduxActionTypes.APP_COLLAB_SET_CONCURRENT_PAGE_EDITORS,
payload,
});

View File

@ -0,0 +1,82 @@
import { ToastComponent } from "components/ads/Toast";
import styled from "styled-components";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { isConcurrentPageEditorToastVisible } from "selectors/appCollabSelectors";
import {
hideConcurrentEditorWarningToast,
getIsConcurrentEditorWarningToastHidden,
} from "utils/storage";
import { Layers } from "constants/Layers";
import { createGlobalStyle } from "styled-components";
const Container = styled.div<{ visible?: boolean }>`
position: fixed;
top: 50px;
transition: right 0.3s linear;
right: ${(props) =>
props.visible ? "1em" : "-500px"}; /* to move away from the viewport */
& {
.concurrent-editing-warning-text {
width: 100%;
overflow: hidden;
}
}
z-index: ${Layers.concurrentEditorWarning};
`;
const ActionElement = styled.span`
cursor: pointer;
display: inline-block;
width: 100%;
text-align: right;
`;
// move existing toast below to make space for the warning toast
const ToastStyle = createGlobalStyle`
.Toastify__toast-container--top-right {
top: 10.5em !important;
}
`;
const getMessage = () => {
const msg = `Someone else is also editing this page. Your changes may get overwritten. Realtime Editing is coming soon.`;
return msg;
};
export default function ConcurrentPageEditorToast() {
const [isForceHidden, setIsForceHidden] = useState(true);
const isVisible = useSelector(isConcurrentPageEditorToastVisible);
useEffect(() => {
(async () => {
const flag = await getIsConcurrentEditorWarningToastHidden();
setIsForceHidden(!!flag);
})();
}, []);
const hidePermanently = () => {
hideConcurrentEditorWarningToast(); // save in persistent storage
setIsForceHidden(true);
};
const showToast = isVisible && !isForceHidden;
return (
<Container visible={showToast}>
{showToast && (
<ToastComponent
actionElement={
<ActionElement onClick={hidePermanently}>Dismiss</ActionElement>
}
contentClassName="concurrent-editing-warning-text "
hideActionElementSpace
text={getMessage()}
width={"327px"}
/>
)}
{showToast && <ToastStyle />}
</Container>
);
}

View File

@ -12,6 +12,7 @@ import DebugButton from "components/editorComponents/Debugger/DebugCTA";
type ToastProps = ToastOptions & type ToastProps = ToastOptions &
CommonComponentProps & { CommonComponentProps & {
contentClassName?: string;
text: string; text: string;
actionElement?: JSX.Element; actionElement?: JSX.Element;
variant?: Variant; variant?: Variant;
@ -20,6 +21,8 @@ type ToastProps = ToastOptions &
dispatchableAction?: { type: ReduxActionType; payload: any }; dispatchableAction?: { type: ReduxActionType; payload: any };
showDebugButton?: boolean; showDebugButton?: boolean;
hideProgressBar?: boolean; hideProgressBar?: boolean;
hideActionElementSpace?: boolean;
width?: string;
}; };
const WrappedToastContainer = styled.div` const WrappedToastContainer = styled.div`
@ -53,8 +56,9 @@ const ToastBody = styled.div<{
variant?: Variant; variant?: Variant;
isUndo?: boolean; isUndo?: boolean;
dispatchableAction?: { type: ReduxActionType; payload: any }; dispatchableAction?: { type: ReduxActionType; payload: any };
width?: string;
}>` }>`
width: 264px; width: ${(props) => props.width || "264px"};
background: ${(props) => props.theme.colors.toast.bg}; background: ${(props) => props.theme.colors.toast.bg};
padding: ${(props) => props.theme.spaces[4]}px padding: ${(props) => props.theme.spaces[4]}px
${(props) => props.theme.spaces[5]}px; ${(props) => props.theme.spaces[5]}px;
@ -115,6 +119,7 @@ const FlexContainer = styled.div`
const ToastTextWrapper = styled.div` const ToastTextWrapper = styled.div`
flex: 1; flex: 1;
min-width: 0;
`; `;
const StyledDebugButton = styled(DebugButton)` const StyledDebugButton = styled(DebugButton)`
@ -125,7 +130,9 @@ const StyledActionText = styled(Text)`
color: ${(props) => props.theme.colors.toast.undoRedoColor} !important; color: ${(props) => props.theme.colors.toast.undoRedoColor} !important;
`; `;
function ToastComponent(props: ToastProps & { undoAction?: () => void }) { export function ToastComponent(
props: ToastProps & { undoAction?: () => void },
) {
const dispatch = useDispatch(); const dispatch = useDispatch();
return ( return (
@ -134,8 +141,9 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) {
dispatchableAction={props.dispatchableAction} dispatchableAction={props.dispatchableAction}
isUndo={!!props.onUndo} isUndo={!!props.onUndo}
variant={props.variant || Variant.info} variant={props.variant || Variant.info}
width={props.width}
> >
<FlexContainer> <FlexContainer style={{ minWidth: 0 }}>
{props.variant === Variant.success ? ( {props.variant === Variant.success ? (
<Icon fillColor={Colors.GREEN} name="success" size={IconSize.XXL} /> <Icon fillColor={Colors.GREEN} name="success" size={IconSize.XXL} />
) : props.variant === Variant.warning ? ( ) : props.variant === Variant.warning ? (
@ -145,10 +153,13 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) {
<Icon name="error" size={IconSize.XXL} /> <Icon name="error" size={IconSize.XXL} />
) : null} ) : null}
<ToastTextWrapper> <ToastTextWrapper>
<Text type={TextType.P1}>{props.text}</Text> <Text className={props.contentClassName} type={TextType.P1}>
{props.text}
</Text>
{props.actionElement && ( {props.actionElement && (
<StyledActionText type={TextType.P1}> <StyledActionText type={TextType.P1}>
&nbsp;{props.actionElement} {!props.hideActionElementSpace ? <>&nbsp;</> : ""}
{props.actionElement}
</StyledActionText> </StyledActionText>
)} )}
{props.variant === Variant.danger && props.showDebugButton ? ( {props.variant === Variant.danger && props.showDebugButton ? (

View File

@ -1,5 +1,11 @@
import React, { useState, useRef, RefObject, useCallback } from "react"; import React, {
import { connect, useSelector } from "react-redux"; useState,
useRef,
RefObject,
useCallback,
useEffect,
} from "react";
import { connect, useDispatch, useSelector } from "react-redux";
import { withRouter, RouteComponentProps } from "react-router"; import { withRouter, RouteComponentProps } from "react-router";
import styled from "styled-components"; import styled from "styled-components";
import { AppState } from "reducers"; import { AppState } from "reducers";
@ -35,6 +41,7 @@ import { DebugButton } from "./Debugger/DebugCTA";
import EntityDeps from "./Debugger/EntityDependecies"; import EntityDeps from "./Debugger/EntityDependecies";
import Button, { Size } from "components/ads/Button"; import Button, { Size } from "components/ads/Button";
import { getActionTabsInitialIndex } from "selectors/editorSelectors"; import { getActionTabsInitialIndex } from "selectors/editorSelectors";
import { setActionTabsInitialIndex } from "actions/pluginActionActions";
type TextStyleProps = { type TextStyleProps = {
accent: "primary" | "secondary" | "error"; accent: "primary" | "secondary" | "error";
@ -225,6 +232,7 @@ function ApiResponseView(props: Props) {
hasFailed = response.statusCode ? response.statusCode[0] !== "2" : false; hasFailed = response.statusCode ? response.statusCode[0] !== "2" : false;
} }
const panelRef: RefObject<HTMLDivElement> = useRef(null); const panelRef: RefObject<HTMLDivElement> = useRef(null);
const dispatch = useDispatch();
const onDebugClick = useCallback(() => { const onDebugClick = useCallback(() => {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", { AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
@ -321,6 +329,17 @@ function ApiResponseView(props: Props) {
}, },
]; ];
useEffect(() => {
if (selectedIndex !== initialIndex) setSelectedIndex(initialIndex);
}, [initialIndex]);
useEffect(() => {
// reset on unmount
return () => {
dispatch(setActionTabsInitialIndex(0));
};
}, []);
const onTabSelect = (index: number) => { const onTabSelect = (index: number) => {
const debuggerTabKeys = ["ERROR", "LOGS"]; const debuggerTabKeys = ["ERROR", "LOGS"];
if ( if (
@ -331,7 +350,7 @@ function ApiResponseView(props: Props) {
tabName: tabs[index].key, tabName: tabs[index].key,
}); });
} }
dispatch(setActionTabsInitialIndex(index));
setSelectedIndex(index); setSelectedIndex(index);
}; };

View File

@ -1,4 +1,3 @@
import { Classes } from "components/ads/common";
import Icon, { IconSize } from "components/ads/Icon"; import Icon, { IconSize } from "components/ads/Icon";
import React from "react"; import React from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
@ -10,55 +9,17 @@ import { showDebugger as showDebuggerAction } from "actions/debuggerActions";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { Colors } from "constants/Colors"; import { Colors } from "constants/Colors";
import { getTypographyByKey } from "constants/DefaultTheme"; import { getTypographyByKey } from "constants/DefaultTheme";
import { Layers } from "constants/Layers";
import { stopEventPropagation } from "utils/AppsmithUtils"; import { stopEventPropagation } from "utils/AppsmithUtils";
import { getMessageCount } from "selectors/debuggerSelectors"; import { getMessageCount } from "selectors/debuggerSelectors";
import getFeatureFlags from "utils/featureFlags"; import { setActionTabsInitialIndex } from "actions/pluginActionActions";
import {
const Container = styled.div<{ errorCount: number; warningCount: number }>` matchApiPath,
z-index: ${Layers.debugger}; matchBuilderPath,
background-color: ${(props) => matchQueryPath,
props.theme.colors.debugger.floatingButton.background}; } from "constants/routes";
position: absolute; import TooltipComponent from "components/ads/Tooltip";
right: 20px;
bottom: 20px;
cursor: pointer;
padding: ${(props) => props.theme.spaces[6]}px;
color: ${(props) => props.theme.colors.debugger.floatingButton.color};
border-radius: 50px;
box-shadow: ${(props) => props.theme.colors.debugger.floatingButton.shadow};
.${Classes.ICON} {
&:hover {
path {
fill: ${(props) => props.theme.colors.icon.normal};
}
}
}
.debugger-count {
color: ${Colors.WHITE};
${(props) => getTypographyByKey(props, "h6")}
height: 16px;
padding: ${(props) => props.theme.spaces[1]}px;
background-color: ${(props) =>
props.errorCount + props.warningCount > 0
? props.errorCount === 0
? props.theme.colors.debugger.floatingButton.warningCount
: props.theme.colors.debugger.floatingButton.errorCount
: props.theme.colors.debugger.floatingButton.noErrorCount};
border-radius: 10px;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
top: 0;
right: 0;
}
`;
function Debugger() { function Debugger() {
const dispatch = useDispatch();
const messageCounters = useSelector(getMessageCount); const messageCounters = useSelector(getMessageCount);
const totalMessageCount = messageCounters.errors + messageCounters.warnings; const totalMessageCount = messageCounters.errors + messageCounters.warnings;
@ -66,30 +27,6 @@ function Debugger() {
(state: AppState) => state.ui.debugger.isOpen, (state: AppState) => state.ui.debugger.isOpen,
); );
const onClick = (e: any) => {
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
source: "CANVAS",
});
dispatch(showDebuggerAction(true));
stopEventPropagation(e);
};
if (!showDebugger && !getFeatureFlags().GIT)
return (
<Container
className="t--debugger"
errorCount={messageCounters.errors}
onClick={onClick}
warningCount={messageCounters.warnings}
>
<Icon name="bug" size={IconSize.XL} />
{!!messageCounters.errors && (
<div className="debugger-count t--debugger-count">
{totalMessageCount}
</div>
)}
</Container>
);
return showDebugger ? ( return showDebugger ? (
<DebuggerTabs defaultIndex={totalMessageCount ? 0 : 1} /> <DebuggerTabs defaultIndex={totalMessageCount ? 0 : 1} />
) : null; ) : null;
@ -103,7 +40,7 @@ const TriggerContainer = styled.div<{
overflow: visible; overflow: visible;
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: ${(props) => props.theme.spaces[9]}px; margin-right: ${(props) => props.theme.spaces[10]}px;
.debugger-count { .debugger-count {
color: ${Colors.WHITE}; color: ${Colors.WHITE};
@ -122,6 +59,7 @@ const TriggerContainer = styled.div<{
justify-content: center; justify-content: center;
top: 0; top: 0;
left: 100%; left: 100%;
border-radius: 50%;
} }
`; `;
@ -136,20 +74,44 @@ export function DebuggerTrigger() {
const totalMessageCount = messageCounters.errors + messageCounters.warnings; const totalMessageCount = messageCounters.errors + messageCounters.warnings;
const onClick = (e: any) => { const onClick = (e: any) => {
const isOnCanvas = matchBuilderPath(window.location.pathname);
if (isOnCanvas) {
dispatch(showDebuggerAction(!showDebugger));
if (!showDebugger) if (!showDebugger)
AnalyticsUtil.logEvent("OPEN_DEBUGGER", { AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
source: "CANVAS", source: "CANVAS",
}); });
dispatch(showDebuggerAction(!showDebugger)); }
const onApiEditor = matchApiPath(window.location.pathname);
const onQueryEditor = matchQueryPath(window.location.pathname);
if (onApiEditor || onQueryEditor) {
dispatch(setActionTabsInitialIndex(1));
}
stopEventPropagation(e); stopEventPropagation(e);
}; };
const tooltipContent =
totalMessageCount > 0
? `View details for ${totalMessageCount} ${
totalMessageCount > 1 ? "errors" : "error"
}`
: "View logs";
return ( return (
<TriggerContainer <TriggerContainer
className="t--debugger"
errorCount={messageCounters.errors} errorCount={messageCounters.errors}
warningCount={messageCounters.warnings} warningCount={messageCounters.warnings}
>
<TooltipComponent
content={tooltipContent}
modifiers={{
preventOverflow: { enabled: true },
}}
> >
<Icon name="bug" onClick={onClick} size={IconSize.XL} /> <Icon name="bug" onClick={onClick} size={IconSize.XL} />
</TooltipComponent>
{!!messageCounters.errors && ( {!!messageCounters.errors && (
<div className="debugger-count t--debugger-count"> <div className="debugger-count t--debugger-count">
{totalMessageCount > 9 ? "9+" : totalMessageCount} {totalMessageCount > 9 ? "9+" : totalMessageCount}

View File

@ -56,6 +56,7 @@ export const Layers = {
max: Indices.LayerMax, max: Indices.LayerMax,
sideStickyBar: Indices.Layer7, sideStickyBar: Indices.Layer7,
evaluationPopper: Indices.Layer3, evaluationPopper: Indices.Layer3,
concurrentEditorWarning: Indices.Layer2,
}; };
export const LayersContext = React.createContext(Layers); export const LayersContext = React.createContext(Layers);

View File

@ -11,6 +11,8 @@ export const ReduxSagaChannels = {
}; };
export const ReduxActionTypes = { export const ReduxActionTypes = {
APP_COLLAB_SET_CONCURRENT_PAGE_EDITORS:
"APP_COLLAB_SET_CONCURRENT_PAGE_EDITORS",
FETCH_SSH_KEY_PAIR_INIT: "FETCH_SSH_KEY_PAIR_INIT", FETCH_SSH_KEY_PAIR_INIT: "FETCH_SSH_KEY_PAIR_INIT",
FETCH_SSH_KEY_PAIR_SUCCESS: "FETCH_SSH_KEY_PAIR_SUCCESS", FETCH_SSH_KEY_PAIR_SUCCESS: "FETCH_SSH_KEY_PAIR_SUCCESS",
SET_IS_IMPORT_APP_VIA_GIT_MODAL_OPEN: "SET_IS_IMPORT_APP_VIA_GIT_MODAL_OPEN", SET_IS_IMPORT_APP_VIA_GIT_MODAL_OPEN: "SET_IS_IMPORT_APP_VIA_GIT_MODAL_OPEN",

View File

@ -3,6 +3,7 @@ import styled from "styled-components";
import QuickGitActions from "pages/Editor/gitSync/QuickGitActions"; import QuickGitActions from "pages/Editor/gitSync/QuickGitActions";
import { Layers } from "constants/Layers"; import { Layers } from "constants/Layers";
import { DebuggerTrigger } from "components/editorComponents/Debugger"; import { DebuggerTrigger } from "components/editorComponents/Debugger";
import { Colors } from "constants/Colors";
const Container = styled.div` const Container = styled.div`
position: relative; position: relative;
@ -12,6 +13,7 @@ const Container = styled.div`
justify-content: space-between; justify-content: space-between;
background-color: ${(props) => props.theme.colors.editorBottomBar.background}; background-color: ${(props) => props.theme.colors.editorBottomBar.background};
z-index: ${Layers.bottomBar}; z-index: ${Layers.bottomBar};
border-top: solid 1px ${Colors.MERCURY};
`; `;
export default function BottomBar() { export default function BottomBar() {

View File

@ -7,8 +7,6 @@ import WidgetsEditor from "./WidgetsEditor";
import Sidebar from "components/editorComponents/Sidebar"; import Sidebar from "components/editorComponents/Sidebar";
import BottomBar from "./BottomBar"; import BottomBar from "./BottomBar";
import getFeatureFlags from "utils/featureFlags";
import { BUILDER_CHECKLIST_URL, BUILDER_URL } from "constants/routes"; import { BUILDER_CHECKLIST_URL, BUILDER_URL } from "constants/routes";
import OnboardingChecklist from "./FirstTimeUserOnboarding/Checklist"; import OnboardingChecklist from "./FirstTimeUserOnboarding/Checklist";
const SentryRoute = Sentry.withSentryRouting(Route); const SentryRoute = Sentry.withSentryRouting(Route);
@ -17,8 +15,7 @@ const Container = styled.div`
display: flex; display: flex;
height: calc( height: calc(
100vh - ${(props) => props.theme.smallHeaderHeight} - 100vh - ${(props) => props.theme.smallHeaderHeight} -
${(props) => ${(props) => props.theme.bottomBarHeight}
getFeatureFlags().GIT ? props.theme.bottomBarHeight : "0px"}
); );
background-color: ${(props) => props.theme.appBackground}; background-color: ${(props) => props.theme.appBackground};
`; `;
@ -47,7 +44,7 @@ function MainContainer() {
</Switch> </Switch>
</EditorContainer> </EditorContainer>
</Container> </Container>
{getFeatureFlags().GIT && <BottomBar />} <BottomBar />
</> </>
); );
} }

View File

@ -1,4 +1,4 @@
import React, { RefObject, useRef, useState } from "react"; import React, { RefObject, useEffect, useRef, useState } from "react";
import { InjectedFormProps } from "redux-form"; import { InjectedFormProps } from "redux-form";
import { Icon, Tag } from "@blueprintjs/core"; import { Icon, Tag } from "@blueprintjs/core";
import { isString } from "lodash"; import { isString } from "lodash";
@ -71,6 +71,7 @@ import TooltipComponent from "components/ads/Tooltip";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import SearchSnippets from "components/ads/SnippetButton"; import SearchSnippets from "components/ads/SnippetButton";
import { setActionTabsInitialIndex } from "actions/pluginActionActions";
const QueryFormContainer = styled.form` const QueryFormContainer = styled.form`
flex: 1; flex: 1;
@ -430,6 +431,17 @@ export function EditorJSONtoForm(props: Props) {
window.innerHeight, window.innerHeight,
); );
useEffect(() => {
if (selectedIndex !== initialIndex) setSelectedIndex(initialIndex);
}, [initialIndex]);
useEffect(() => {
// reset on unmount
return () => {
dispatch(setActionTabsInitialIndex(0));
};
}, []);
const params = useParams<{ apiId?: string; queryId?: string }>(); const params = useParams<{ apiId?: string; queryId?: string }>();
const actions: Action[] = useSelector((state: AppState) => const actions: Action[] = useSelector((state: AppState) =>
@ -729,7 +741,7 @@ export function EditorJSONtoForm(props: Props) {
tabName: responseTabs[index].key, tabName: responseTabs[index].key,
}); });
} }
dispatch(setActionTabsInitialIndex(index));
setSelectedIndex(index); setSelectedIndex(index);
}; };
const { entityDependencies, hasDependencies } = useEntityDependencies( const { entityDependencies, hasDependencies } = useEntityDependencies(

View File

@ -10,6 +10,7 @@ import {
collabStopEditingAppEvent, collabStopEditingAppEvent,
collabResetAppEditors, collabResetAppEditors,
} from "actions/appCollabActions"; } from "actions/appCollabActions";
import { getCurrentPageId } from "selectors/editorSelectors";
import { getIsAppLevelSocketConnected } from "selectors/websocketSelectors"; import { getIsAppLevelSocketConnected } from "selectors/websocketSelectors";
const UserImageContainer = styled.div` const UserImageContainer = styled.div`
@ -37,6 +38,8 @@ export function useEditAppCollabEvents(applicationId?: string) {
const isWebsocketConnected = useSelector(getIsAppLevelSocketConnected); const isWebsocketConnected = useSelector(getIsAppLevelSocketConnected);
const currentPageId = useSelector(getCurrentPageId);
useEffect(() => { useEffect(() => {
// websocket has to be connected as we only fire this event once. // websocket has to be connected as we only fire this event once.
isWebsocketConnected && isWebsocketConnected &&
@ -48,7 +51,7 @@ export function useEditAppCollabEvents(applicationId?: string) {
applicationId && applicationId &&
dispatch(collabStopEditingAppEvent(applicationId)); dispatch(collabStopEditingAppEvent(applicationId));
}; };
}, [applicationId, isWebsocketConnected]); }, [applicationId, currentPageId, isWebsocketConnected]);
} }
function RealtimeAppEditors(props: RealtimeAppEditorsProps) { function RealtimeAppEditors(props: RealtimeAppEditorsProps) {

View File

@ -27,6 +27,7 @@ import { ReactComponent as GitCommitLine } from "assets/icons/ads/git-commit-lin
import Button, { Category, Size } from "components/ads/Button"; import Button, { Category, Size } from "components/ads/Button";
import { setIsGitSyncModalOpen } from "actions/gitSyncActions"; import { setIsGitSyncModalOpen } from "actions/gitSyncActions";
import { GitSyncModalTab } from "entities/GitSync"; import { GitSyncModalTab } from "entities/GitSync";
import getFeatureFlags from "utils/featureFlags";
type QuickActionButtonProps = { type QuickActionButtonProps = {
count?: number; count?: number;
@ -119,6 +120,23 @@ const Container = styled.div`
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: ${(props) => props.theme.spaces[10]}px;
`;
const StyledIcon = styled(GitCommitLine)`
& path {
fill: ${Colors.DARK_GRAY};
}
`;
const PlaceholderButton = styled.div`
padding: ${(props) =>
`${props.theme.spaces[1]}px ${props.theme.spaces[3]}px`};
border: solid 1px ${Colors.MERCURY};
${(props) => getTypographyByKey(props, "btnSmall")};
text-transform: uppercase;
background-color: ${Colors.ALABASTER_ALT};
color: ${Colors.GRAY};
`; `;
function ConnectGitPlaceholder() { function ConnectGitPlaceholder() {
@ -126,7 +144,21 @@ function ConnectGitPlaceholder() {
return ( return (
<Container> <Container>
<GitCommitLine /> <Tooltip
content={
<>
<div>It&apos;s not live for you yet</div>
<div>Coming soon!</div>
</>
}
disabled={getFeatureFlags().GIT}
modifiers={{
preventOverflow: { enabled: true },
}}
>
<Container style={{ marginLeft: 0, cursor: "pointer" }}>
<StyledIcon />
{getFeatureFlags().GIT ? (
<Button <Button
category={Category.tertiary} category={Category.tertiary}
onClick={() => { onClick={() => {
@ -135,6 +167,11 @@ function ConnectGitPlaceholder() {
size={Size.small} size={Size.small}
text={createMessage(CONNECT_GIT)} text={createMessage(CONNECT_GIT)}
/> />
) : (
<PlaceholderButton>{createMessage(CONNECT_GIT)}</PlaceholderButton>
)}
</Container>
</Tooltip>
</Container> </Container>
); );
} }
@ -163,7 +200,7 @@ export default function QuickGitActions() {
); );
}, },
}); });
return isGitRepoSetup ? ( return getFeatureFlags().GIT && isGitRepoSetup ? (
<Container> <Container>
<BranchButton /> <BranchButton />
{quickActionButtons.map((button) => ( {quickActionButtons.map((button) => (

View File

@ -36,6 +36,13 @@ import GitSyncModal from "pages/Editor/gitSync/GitSyncModal";
import history from "utils/history"; import history from "utils/history";
import { fetchPage, updateCurrentPage } from "actions/pageActions"; import { fetchPage, updateCurrentPage } from "actions/pageActions";
import ConcurrentPageEditorToast from "comments/ConcurrentPageEditorToast";
import { getIsPageLevelSocketConnected } from "selectors/websocketSelectors";
import {
collabStartSharingPointerEvent,
collabStopSharingPointerEvent,
} from "actions/appCollabActions";
type EditorProps = { type EditorProps = {
currentApplicationId?: string; currentApplicationId?: string;
currentApplicationName?: string; currentApplicationName?: string;
@ -52,6 +59,9 @@ type EditorProps = {
handlePathUpdated: (location: typeof window.location) => void; handlePathUpdated: (location: typeof window.location) => void;
fetchPage: (pageId: string) => void; fetchPage: (pageId: string) => void;
updateCurrentPage: (pageId: string) => void; updateCurrentPage: (pageId: string) => void;
isPageLevelSocketConnected: boolean;
collabStartSharingPointerEvent: (pageId: string) => void;
collabStopSharingPointerEvent: () => void;
}; };
type Props = EditorProps & RouteComponentProps<BuilderRouteParams>; type Props = EditorProps & RouteComponentProps<BuilderRouteParams>;
@ -73,6 +83,10 @@ class Editor extends Component<Props> {
} }
this.props.handlePathUpdated(window.location); this.props.handlePathUpdated(window.location);
this.unlisten = history.listen(this.handleHistoryChange); this.unlisten = history.listen(this.handleHistoryChange);
if (this.props.isPageLevelSocketConnected && pageId) {
this.props.collabStartSharingPointerEvent(pageId);
}
} }
shouldComponentUpdate(nextProps: Props, nextState: { registered: boolean }) { shouldComponentUpdate(nextProps: Props, nextState: { registered: boolean }) {
@ -88,22 +102,30 @@ class Editor extends Component<Props> {
this.props.isEditorInitializeError || this.props.isEditorInitializeError ||
nextProps.creatingOnboardingDatabase !== nextProps.creatingOnboardingDatabase !==
this.props.creatingOnboardingDatabase || this.props.creatingOnboardingDatabase ||
nextState.registered !== this.state.registered nextState.registered !== this.state.registered ||
(nextProps.isPageLevelSocketConnected &&
!this.props.isPageLevelSocketConnected)
); );
} }
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
const { pageId } = this.props.match.params || {}; const { pageId } = this.props.match.params || {};
const { pageId: prevPageId } = prevProps.match.params || {}; const { pageId: prevPageId } = prevProps.match.params || {};
if (pageId && pageId !== prevPageId) { const isPageIdUpdated = pageId !== prevPageId;
if (pageId && isPageIdUpdated) {
this.props.updateCurrentPage(pageId); this.props.updateCurrentPage(pageId);
this.props.fetchPage(pageId); this.props.fetchPage(pageId);
} }
if (this.props.isPageLevelSocketConnected && isPageIdUpdated) {
this.props.collabStartSharingPointerEvent(pageId);
}
} }
componentWillUnmount() { componentWillUnmount() {
this.props.resetEditorRequest(); this.props.resetEditorRequest();
if (typeof this.unlisten === "function") this.unlisten(); if (typeof this.unlisten === "function") this.unlisten();
this.props.collabStopSharingPointerEvent();
} }
handleHistoryChange = (location: any) => { handleHistoryChange = (location: any) => {
@ -143,6 +165,7 @@ class Editor extends Component<Props> {
<AddCommentTourComponent /> <AddCommentTourComponent />
<CommentShowCaseCarousel /> <CommentShowCaseCarousel />
<GitSyncModal /> <GitSyncModal />
<ConcurrentPageEditorToast />
</GlobalHotKeys> </GlobalHotKeys>
</div> </div>
<ConfirmRunModal /> <ConfirmRunModal />
@ -163,6 +186,7 @@ const mapStateToProps = (state: AppState) => ({
user: getCurrentUser(state), user: getCurrentUser(state),
creatingOnboardingDatabase: state.ui.onBoarding.showOnboardingLoader, creatingOnboardingDatabase: state.ui.onBoarding.showOnboardingLoader,
currentApplicationName: state.ui.applications.currentApplication?.name, currentApplicationName: state.ui.applications.currentApplication?.name,
isPageLevelSocketConnected: getIsPageLevelSocketConnected(state),
}); });
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
@ -174,6 +198,10 @@ const mapDispatchToProps = (dispatch: any) => {
dispatch(handlePathUpdated(location)), dispatch(handlePathUpdated(location)),
fetchPage: (pageId: string) => dispatch(fetchPage(pageId)), fetchPage: (pageId: string) => dispatch(fetchPage(pageId)),
updateCurrentPage: (pageId: string) => dispatch(updateCurrentPage(pageId)), updateCurrentPage: (pageId: string) => dispatch(updateCurrentPage(pageId)),
collabStartSharingPointerEvent: (pageId: string) =>
dispatch(collabStartSharingPointerEvent(pageId)),
collabStopSharingPointerEvent: () =>
dispatch(collabStopSharingPointerEvent()),
}; };
}; };

View File

@ -2,7 +2,6 @@ import { getDependenciesFromInverseDependencies } from "components/editorCompone
import _, { debounce } from "lodash"; import _, { debounce } from "lodash";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import ResizeObserver from "resize-observer-polyfill"; import ResizeObserver from "resize-observer-polyfill";
import getFeatureFlags from "utils/featureFlags";
export const draggableElement = ( export const draggableElement = (
id: string, id: string,
@ -41,7 +40,7 @@ export const draggableElement = (
calculatedLeft: number, calculatedLeft: number,
calculatedTop: number, calculatedTop: number,
) => { ) => {
const bottomBarOffset = getFeatureFlags().GIT ? 34 : 0; const bottomBarOffset = 34;
if (calculatedLeft <= 0) { if (calculatedLeft <= 0) {
calculatedLeft = 0; calculatedLeft = 0;

View File

@ -6,6 +6,7 @@ import { cloneDeep } from "lodash";
const initialState: AppCollabReducerState = { const initialState: AppCollabReducerState = {
editors: [], editors: [],
pointerData: {}, pointerData: {},
pageEditors: [],
}; };
const appCollabReducer = createReducer(initialState, { const appCollabReducer = createReducer(initialState, {
@ -51,6 +52,13 @@ const appCollabReducer = createReducer(initialState, {
pointerData: {}, pointerData: {},
}; };
}, },
[ReduxActionTypes.APP_COLLAB_SET_CONCURRENT_PAGE_EDITORS]: (
state: AppCollabReducerState,
action: ReduxAction<any>,
) => ({
...state,
pageEditors: action.payload,
}),
}); });
type PointerDataType = { type PointerDataType = {
@ -60,6 +68,7 @@ type PointerDataType = {
export type AppCollabReducerState = { export type AppCollabReducerState = {
editors: User[]; editors: User[];
pointerData: PointerDataType; pointerData: PointerDataType;
pageEditors: User[];
}; };
export default appCollabReducer; export default appCollabReducer;

View File

@ -1,8 +1,5 @@
import { put, select } from "redux-saga/effects"; import { put, select } from "redux-saga/effects";
import { import { APP_LEVEL_SOCKET_EVENTS } from "./socketEvents";
APP_LEVEL_SOCKET_EVENTS,
PAGE_LEVEL_SOCKET_EVENTS,
} from "./socketEvents";
import { import {
newCommentEvent, newCommentEvent,
@ -96,7 +93,7 @@ export default function* handleAppLevelSocketEvents(event: any) {
return; return;
} }
// Collab V2 - Realtime Editing // Collab V2 - Realtime Editing
case PAGE_LEVEL_SOCKET_EVENTS.LIST_ONLINE_APP_EDITORS: { case APP_LEVEL_SOCKET_EVENTS.LIST_ONLINE_APP_EDITORS: {
yield put(collabSetAppEditors(event.payload[0])); yield put(collabSetAppEditors(event.payload[0]));
return; return;
} }

View File

@ -3,6 +3,7 @@ import { PAGE_LEVEL_SOCKET_EVENTS } from "./socketEvents";
import { import {
collabSetEditorsPointersData, collabSetEditorsPointersData,
collabUnsetEditorsPointersData, collabUnsetEditorsPointersData,
collabConcurrentPageEditorsData,
} from "actions/appCollabActions"; } from "actions/appCollabActions";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
@ -19,6 +20,11 @@ export default function* handlePageLevelSocketEvents(event: any, socket: any) {
yield put(collabUnsetEditorsPointersData(event.payload[0])); yield put(collabUnsetEditorsPointersData(event.payload[0]));
return; return;
} }
case PAGE_LEVEL_SOCKET_EVENTS.LIST_ONLINE_PAGE_EDITORS: {
yield put(collabConcurrentPageEditorsData(event.payload[0]?.users));
return;
}
} }
} catch (e) { } catch (e) {
Sentry.captureException(e); Sentry.captureException(e);

View File

@ -16,11 +16,13 @@ export const APP_LEVEL_SOCKET_EVENTS = {
// notification events // notification events
INSERT_NOTIFICATION: "insert:notification", INSERT_NOTIFICATION: "insert:notification",
LIST_ONLINE_APP_EDITORS: "collab:online_editors", // user presence
}; };
export const PAGE_LEVEL_SOCKET_EVENTS = { export const PAGE_LEVEL_SOCKET_EVENTS = {
START_EDITING_APP: "collab:start_edit", START_EDITING_APP: "collab:start_edit",
STOP_EDITING_APP: "collab:leave_edit", STOP_EDITING_APP: "collab:leave_edit",
LIST_ONLINE_APP_EDITORS: "collab:online_editors", // user presence LIST_ONLINE_PAGE_EDITORS: "collab:online_editors",
SHARE_USER_POINTER: "collab:mouse_pointer", // multi pointer SHARE_USER_POINTER: "collab:mouse_pointer", // multi pointer
}; };

View File

@ -3,6 +3,8 @@ import { AppState } from "reducers";
import { AppCollabReducerState } from "reducers/uiReducers/appCollabReducer"; import { AppCollabReducerState } from "reducers/uiReducers/appCollabReducer";
import { getCurrentUser } from "./usersSelectors"; import { getCurrentUser } from "./usersSelectors";
import getFeatureFlags from "../utils/featureFlags"; import getFeatureFlags from "../utils/featureFlags";
import { User } from "entities/AppCollab/CollabInterfaces";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
export const getAppCollabState = (state: AppState) => state.ui.appCollab; export const getAppCollabState = (state: AppState) => state.ui.appCollab;
@ -14,3 +16,22 @@ export const getRealtimeAppEditors = createSelector(
); );
export const isMultiplayerEnabledForUser = () => getFeatureFlags().MULTIPLAYER; export const isMultiplayerEnabledForUser = () => getFeatureFlags().MULTIPLAYER;
export const getConcurrentPageEditors = (state: AppState) =>
state.ui.appCollab.pageEditors;
export const isConcurrentPageEditorToastVisible = createSelector(
getConcurrentPageEditors,
getCurrentUser,
(pageEditors: User[], currentUser?: User) => {
if (
pageEditors.length === 0 ||
!currentUser ||
currentUser.email === ANONYMOUS_USERNAME
)
return;
return pageEditors.some(
(editor: User) => editor.email !== currentUser?.email,
);
},
);

View File

@ -17,6 +17,7 @@ const STORAGE_KEYS: { [id: string]: string } = {
"FIRST_TIME_USER_ONBOARDING_APPLICATION_ID", "FIRST_TIME_USER_ONBOARDING_APPLICATION_ID",
FIRST_TIME_USER_ONBOARDING_INTRO_MODAL_VISIBILITY: FIRST_TIME_USER_ONBOARDING_INTRO_MODAL_VISIBILITY:
"FIRST_TIME_USER_ONBOARDING_INTRO_MODAL_VISIBILITY", "FIRST_TIME_USER_ONBOARDING_INTRO_MODAL_VISIBILITY",
HIDE_CONCURRENT_EDITOR_WARNING_TOAST: "HIDE_CONCURRENT_EDITOR_WARNING_TOAST",
}; };
const store = localforage.createInstance({ const store = localforage.createInstance({
@ -261,3 +262,32 @@ export const getFirstTimeUserOnboardingIntroModalVisibility = async () => {
log.error(error); log.error(error);
} }
}; };
export const hideConcurrentEditorWarningToast = async () => {
try {
await store.setItem(
STORAGE_KEYS.HIDE_CONCURRENT_EDITOR_WARNING_TOAST,
true,
);
return true;
} catch (error) {
log.error(
"An error occurred while setting HIDE_CONCURRENT_EDITOR_WARNING_TOAST",
);
log.error(error);
}
};
export const getIsConcurrentEditorWarningToastHidden = async () => {
try {
const flag = await store.getItem(
STORAGE_KEYS.HIDE_CONCURRENT_EDITOR_WARNING_TOAST,
);
return flag;
} catch (error) {
log.error(
"An error occurred while fetching HIDE_CONCURRENT_EDITOR_WARNING_TOAST",
);
log.error(error);
}
};