feat: callouts for wip features (#8080)
This commit is contained in:
parent
bacf29848b
commit
a69c8b7484
|
|
@ -16,7 +16,7 @@ describe("Unique react keys", function() {
|
|||
cy.dragAndDropToCanvas("chartwidget", { x: 200, y: 200 });
|
||||
|
||||
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.deleteWidget(widgetsPage.chartWidget);
|
||||
|
|
|
|||
|
|
@ -60,3 +60,8 @@ export const collabUnsetEditorsPointersData = (payload: any) => ({
|
|||
export const collabResetEditorsPointersData = () => ({
|
||||
type: ReduxActionTypes.APP_COLLAB_RESET_EDITORS_POINTER_DATA,
|
||||
});
|
||||
|
||||
export const collabConcurrentPageEditorsData = (payload: any) => ({
|
||||
type: ReduxActionTypes.APP_COLLAB_SET_CONCURRENT_PAGE_EDITORS,
|
||||
payload,
|
||||
});
|
||||
|
|
|
|||
82
app/client/src/comments/ConcurrentPageEditorToast.tsx
Normal file
82
app/client/src/comments/ConcurrentPageEditorToast.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import DebugButton from "components/editorComponents/Debugger/DebugCTA";
|
|||
|
||||
type ToastProps = ToastOptions &
|
||||
CommonComponentProps & {
|
||||
contentClassName?: string;
|
||||
text: string;
|
||||
actionElement?: JSX.Element;
|
||||
variant?: Variant;
|
||||
|
|
@ -20,6 +21,8 @@ type ToastProps = ToastOptions &
|
|||
dispatchableAction?: { type: ReduxActionType; payload: any };
|
||||
showDebugButton?: boolean;
|
||||
hideProgressBar?: boolean;
|
||||
hideActionElementSpace?: boolean;
|
||||
width?: string;
|
||||
};
|
||||
|
||||
const WrappedToastContainer = styled.div`
|
||||
|
|
@ -53,8 +56,9 @@ const ToastBody = styled.div<{
|
|||
variant?: Variant;
|
||||
isUndo?: boolean;
|
||||
dispatchableAction?: { type: ReduxActionType; payload: any };
|
||||
width?: string;
|
||||
}>`
|
||||
width: 264px;
|
||||
width: ${(props) => props.width || "264px"};
|
||||
background: ${(props) => props.theme.colors.toast.bg};
|
||||
padding: ${(props) => props.theme.spaces[4]}px
|
||||
${(props) => props.theme.spaces[5]}px;
|
||||
|
|
@ -115,6 +119,7 @@ const FlexContainer = styled.div`
|
|||
|
||||
const ToastTextWrapper = styled.div`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledDebugButton = styled(DebugButton)`
|
||||
|
|
@ -125,7 +130,9 @@ const StyledActionText = styled(Text)`
|
|||
color: ${(props) => props.theme.colors.toast.undoRedoColor} !important;
|
||||
`;
|
||||
|
||||
function ToastComponent(props: ToastProps & { undoAction?: () => void }) {
|
||||
export function ToastComponent(
|
||||
props: ToastProps & { undoAction?: () => void },
|
||||
) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
|
|
@ -134,8 +141,9 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) {
|
|||
dispatchableAction={props.dispatchableAction}
|
||||
isUndo={!!props.onUndo}
|
||||
variant={props.variant || Variant.info}
|
||||
width={props.width}
|
||||
>
|
||||
<FlexContainer>
|
||||
<FlexContainer style={{ minWidth: 0 }}>
|
||||
{props.variant === Variant.success ? (
|
||||
<Icon fillColor={Colors.GREEN} name="success" size={IconSize.XXL} />
|
||||
) : props.variant === Variant.warning ? (
|
||||
|
|
@ -145,10 +153,13 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) {
|
|||
<Icon name="error" size={IconSize.XXL} />
|
||||
) : null}
|
||||
<ToastTextWrapper>
|
||||
<Text type={TextType.P1}>{props.text}</Text>
|
||||
<Text className={props.contentClassName} type={TextType.P1}>
|
||||
{props.text}
|
||||
</Text>
|
||||
{props.actionElement && (
|
||||
<StyledActionText type={TextType.P1}>
|
||||
{props.actionElement}
|
||||
{!props.hideActionElementSpace ? <> </> : ""}
|
||||
{props.actionElement}
|
||||
</StyledActionText>
|
||||
)}
|
||||
{props.variant === Variant.danger && props.showDebugButton ? (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import React, { useState, useRef, RefObject, useCallback } from "react";
|
||||
import { connect, useSelector } from "react-redux";
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { connect, useDispatch, useSelector } from "react-redux";
|
||||
import { withRouter, RouteComponentProps } from "react-router";
|
||||
import styled from "styled-components";
|
||||
import { AppState } from "reducers";
|
||||
|
|
@ -35,6 +41,7 @@ import { DebugButton } from "./Debugger/DebugCTA";
|
|||
import EntityDeps from "./Debugger/EntityDependecies";
|
||||
import Button, { Size } from "components/ads/Button";
|
||||
import { getActionTabsInitialIndex } from "selectors/editorSelectors";
|
||||
import { setActionTabsInitialIndex } from "actions/pluginActionActions";
|
||||
|
||||
type TextStyleProps = {
|
||||
accent: "primary" | "secondary" | "error";
|
||||
|
|
@ -225,6 +232,7 @@ function ApiResponseView(props: Props) {
|
|||
hasFailed = response.statusCode ? response.statusCode[0] !== "2" : false;
|
||||
}
|
||||
const panelRef: RefObject<HTMLDivElement> = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDebugClick = useCallback(() => {
|
||||
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 debuggerTabKeys = ["ERROR", "LOGS"];
|
||||
if (
|
||||
|
|
@ -331,7 +350,7 @@ function ApiResponseView(props: Props) {
|
|||
tabName: tabs[index].key,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(setActionTabsInitialIndex(index));
|
||||
setSelectedIndex(index);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Classes } from "components/ads/common";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
|
@ -10,55 +9,17 @@ import { showDebugger as showDebuggerAction } from "actions/debuggerActions";
|
|||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { Colors } from "constants/Colors";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import { Layers } from "constants/Layers";
|
||||
import { stopEventPropagation } from "utils/AppsmithUtils";
|
||||
import { getMessageCount } from "selectors/debuggerSelectors";
|
||||
import getFeatureFlags from "utils/featureFlags";
|
||||
|
||||
const Container = styled.div<{ errorCount: number; warningCount: number }>`
|
||||
z-index: ${Layers.debugger};
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.debugger.floatingButton.background};
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
`;
|
||||
import { setActionTabsInitialIndex } from "actions/pluginActionActions";
|
||||
import {
|
||||
matchApiPath,
|
||||
matchBuilderPath,
|
||||
matchQueryPath,
|
||||
} from "constants/routes";
|
||||
import TooltipComponent from "components/ads/Tooltip";
|
||||
|
||||
function Debugger() {
|
||||
const dispatch = useDispatch();
|
||||
const messageCounters = useSelector(getMessageCount);
|
||||
|
||||
const totalMessageCount = messageCounters.errors + messageCounters.warnings;
|
||||
|
|
@ -66,30 +27,6 @@ function Debugger() {
|
|||
(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 ? (
|
||||
<DebuggerTabs defaultIndex={totalMessageCount ? 0 : 1} />
|
||||
) : null;
|
||||
|
|
@ -103,7 +40,7 @@ const TriggerContainer = styled.div<{
|
|||
overflow: visible;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: ${(props) => props.theme.spaces[9]}px;
|
||||
margin-right: ${(props) => props.theme.spaces[10]}px;
|
||||
|
||||
.debugger-count {
|
||||
color: ${Colors.WHITE};
|
||||
|
|
@ -122,6 +59,7 @@ const TriggerContainer = styled.div<{
|
|||
justify-content: center;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -136,20 +74,44 @@ export function DebuggerTrigger() {
|
|||
const totalMessageCount = messageCounters.errors + messageCounters.warnings;
|
||||
|
||||
const onClick = (e: any) => {
|
||||
if (!showDebugger)
|
||||
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
|
||||
source: "CANVAS",
|
||||
});
|
||||
dispatch(showDebuggerAction(!showDebugger));
|
||||
const isOnCanvas = matchBuilderPath(window.location.pathname);
|
||||
if (isOnCanvas) {
|
||||
dispatch(showDebuggerAction(!showDebugger));
|
||||
if (!showDebugger)
|
||||
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
|
||||
source: "CANVAS",
|
||||
});
|
||||
}
|
||||
|
||||
const onApiEditor = matchApiPath(window.location.pathname);
|
||||
const onQueryEditor = matchQueryPath(window.location.pathname);
|
||||
if (onApiEditor || onQueryEditor) {
|
||||
dispatch(setActionTabsInitialIndex(1));
|
||||
}
|
||||
stopEventPropagation(e);
|
||||
};
|
||||
|
||||
const tooltipContent =
|
||||
totalMessageCount > 0
|
||||
? `View details for ${totalMessageCount} ${
|
||||
totalMessageCount > 1 ? "errors" : "error"
|
||||
}`
|
||||
: "View logs";
|
||||
|
||||
return (
|
||||
<TriggerContainer
|
||||
className="t--debugger"
|
||||
errorCount={messageCounters.errors}
|
||||
warningCount={messageCounters.warnings}
|
||||
>
|
||||
<Icon name="bug" onClick={onClick} size={IconSize.XL} />
|
||||
<TooltipComponent
|
||||
content={tooltipContent}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: true },
|
||||
}}
|
||||
>
|
||||
<Icon name="bug" onClick={onClick} size={IconSize.XL} />
|
||||
</TooltipComponent>
|
||||
{!!messageCounters.errors && (
|
||||
<div className="debugger-count t--debugger-count">
|
||||
{totalMessageCount > 9 ? "9+" : totalMessageCount}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export const Layers = {
|
|||
max: Indices.LayerMax,
|
||||
sideStickyBar: Indices.Layer7,
|
||||
evaluationPopper: Indices.Layer3,
|
||||
concurrentEditorWarning: Indices.Layer2,
|
||||
};
|
||||
|
||||
export const LayersContext = React.createContext(Layers);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export const ReduxSagaChannels = {
|
|||
};
|
||||
|
||||
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_SUCCESS: "FETCH_SSH_KEY_PAIR_SUCCESS",
|
||||
SET_IS_IMPORT_APP_VIA_GIT_MODAL_OPEN: "SET_IS_IMPORT_APP_VIA_GIT_MODAL_OPEN",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import styled from "styled-components";
|
|||
import QuickGitActions from "pages/Editor/gitSync/QuickGitActions";
|
||||
import { Layers } from "constants/Layers";
|
||||
import { DebuggerTrigger } from "components/editorComponents/Debugger";
|
||||
import { Colors } from "constants/Colors";
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
|
|
@ -12,6 +13,7 @@ const Container = styled.div`
|
|||
justify-content: space-between;
|
||||
background-color: ${(props) => props.theme.colors.editorBottomBar.background};
|
||||
z-index: ${Layers.bottomBar};
|
||||
border-top: solid 1px ${Colors.MERCURY};
|
||||
`;
|
||||
|
||||
export default function BottomBar() {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import WidgetsEditor from "./WidgetsEditor";
|
|||
import Sidebar from "components/editorComponents/Sidebar";
|
||||
import BottomBar from "./BottomBar";
|
||||
|
||||
import getFeatureFlags from "utils/featureFlags";
|
||||
|
||||
import { BUILDER_CHECKLIST_URL, BUILDER_URL } from "constants/routes";
|
||||
import OnboardingChecklist from "./FirstTimeUserOnboarding/Checklist";
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
|
@ -17,8 +15,7 @@ const Container = styled.div`
|
|||
display: flex;
|
||||
height: calc(
|
||||
100vh - ${(props) => props.theme.smallHeaderHeight} -
|
||||
${(props) =>
|
||||
getFeatureFlags().GIT ? props.theme.bottomBarHeight : "0px"}
|
||||
${(props) => props.theme.bottomBarHeight}
|
||||
);
|
||||
background-color: ${(props) => props.theme.appBackground};
|
||||
`;
|
||||
|
|
@ -47,7 +44,7 @@ function MainContainer() {
|
|||
</Switch>
|
||||
</EditorContainer>
|
||||
</Container>
|
||||
{getFeatureFlags().GIT && <BottomBar />}
|
||||
<BottomBar />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { Icon, Tag } from "@blueprintjs/core";
|
||||
import { isString } from "lodash";
|
||||
|
|
@ -71,6 +71,7 @@ import TooltipComponent from "components/ads/Tooltip";
|
|||
import * as Sentry from "@sentry/react";
|
||||
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
|
||||
import SearchSnippets from "components/ads/SnippetButton";
|
||||
import { setActionTabsInitialIndex } from "actions/pluginActionActions";
|
||||
|
||||
const QueryFormContainer = styled.form`
|
||||
flex: 1;
|
||||
|
|
@ -430,6 +431,17 @@ export function EditorJSONtoForm(props: Props) {
|
|||
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 actions: Action[] = useSelector((state: AppState) =>
|
||||
|
|
@ -729,7 +741,7 @@ export function EditorJSONtoForm(props: Props) {
|
|||
tabName: responseTabs[index].key,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(setActionTabsInitialIndex(index));
|
||||
setSelectedIndex(index);
|
||||
};
|
||||
const { entityDependencies, hasDependencies } = useEntityDependencies(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
collabStopEditingAppEvent,
|
||||
collabResetAppEditors,
|
||||
} from "actions/appCollabActions";
|
||||
import { getCurrentPageId } from "selectors/editorSelectors";
|
||||
import { getIsAppLevelSocketConnected } from "selectors/websocketSelectors";
|
||||
|
||||
const UserImageContainer = styled.div`
|
||||
|
|
@ -37,6 +38,8 @@ export function useEditAppCollabEvents(applicationId?: string) {
|
|||
|
||||
const isWebsocketConnected = useSelector(getIsAppLevelSocketConnected);
|
||||
|
||||
const currentPageId = useSelector(getCurrentPageId);
|
||||
|
||||
useEffect(() => {
|
||||
// websocket has to be connected as we only fire this event once.
|
||||
isWebsocketConnected &&
|
||||
|
|
@ -48,7 +51,7 @@ export function useEditAppCollabEvents(applicationId?: string) {
|
|||
applicationId &&
|
||||
dispatch(collabStopEditingAppEvent(applicationId));
|
||||
};
|
||||
}, [applicationId, isWebsocketConnected]);
|
||||
}, [applicationId, currentPageId, isWebsocketConnected]);
|
||||
}
|
||||
|
||||
function RealtimeAppEditors(props: RealtimeAppEditorsProps) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { ReactComponent as GitCommitLine } from "assets/icons/ads/git-commit-lin
|
|||
import Button, { Category, Size } from "components/ads/Button";
|
||||
import { setIsGitSyncModalOpen } from "actions/gitSyncActions";
|
||||
import { GitSyncModalTab } from "entities/GitSync";
|
||||
import getFeatureFlags from "utils/featureFlags";
|
||||
|
||||
type QuickActionButtonProps = {
|
||||
count?: number;
|
||||
|
|
@ -119,6 +120,23 @@ const Container = styled.div`
|
|||
height: 100%;
|
||||
display: flex;
|
||||
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() {
|
||||
|
|
@ -126,15 +144,34 @@ function ConnectGitPlaceholder() {
|
|||
|
||||
return (
|
||||
<Container>
|
||||
<GitCommitLine />
|
||||
<Button
|
||||
category={Category.tertiary}
|
||||
onClick={() => {
|
||||
dispatch(setIsGitSyncModalOpen({ isOpen: true }));
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<div>It's not live for you yet</div>
|
||||
<div>Coming soon!</div>
|
||||
</>
|
||||
}
|
||||
disabled={getFeatureFlags().GIT}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: true },
|
||||
}}
|
||||
size={Size.small}
|
||||
text={createMessage(CONNECT_GIT)}
|
||||
/>
|
||||
>
|
||||
<Container style={{ marginLeft: 0, cursor: "pointer" }}>
|
||||
<StyledIcon />
|
||||
{getFeatureFlags().GIT ? (
|
||||
<Button
|
||||
category={Category.tertiary}
|
||||
onClick={() => {
|
||||
dispatch(setIsGitSyncModalOpen({ isOpen: true }));
|
||||
}}
|
||||
size={Size.small}
|
||||
text={createMessage(CONNECT_GIT)}
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderButton>{createMessage(CONNECT_GIT)}</PlaceholderButton>
|
||||
)}
|
||||
</Container>
|
||||
</Tooltip>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -163,7 +200,7 @@ export default function QuickGitActions() {
|
|||
);
|
||||
},
|
||||
});
|
||||
return isGitRepoSetup ? (
|
||||
return getFeatureFlags().GIT && isGitRepoSetup ? (
|
||||
<Container>
|
||||
<BranchButton />
|
||||
{quickActionButtons.map((button) => (
|
||||
|
|
|
|||
|
|
@ -36,6 +36,13 @@ import GitSyncModal from "pages/Editor/gitSync/GitSyncModal";
|
|||
import history from "utils/history";
|
||||
import { fetchPage, updateCurrentPage } from "actions/pageActions";
|
||||
|
||||
import ConcurrentPageEditorToast from "comments/ConcurrentPageEditorToast";
|
||||
import { getIsPageLevelSocketConnected } from "selectors/websocketSelectors";
|
||||
import {
|
||||
collabStartSharingPointerEvent,
|
||||
collabStopSharingPointerEvent,
|
||||
} from "actions/appCollabActions";
|
||||
|
||||
type EditorProps = {
|
||||
currentApplicationId?: string;
|
||||
currentApplicationName?: string;
|
||||
|
|
@ -52,6 +59,9 @@ type EditorProps = {
|
|||
handlePathUpdated: (location: typeof window.location) => void;
|
||||
fetchPage: (pageId: string) => void;
|
||||
updateCurrentPage: (pageId: string) => void;
|
||||
isPageLevelSocketConnected: boolean;
|
||||
collabStartSharingPointerEvent: (pageId: string) => void;
|
||||
collabStopSharingPointerEvent: () => void;
|
||||
};
|
||||
|
||||
type Props = EditorProps & RouteComponentProps<BuilderRouteParams>;
|
||||
|
|
@ -73,6 +83,10 @@ class Editor extends Component<Props> {
|
|||
}
|
||||
this.props.handlePathUpdated(window.location);
|
||||
this.unlisten = history.listen(this.handleHistoryChange);
|
||||
|
||||
if (this.props.isPageLevelSocketConnected && pageId) {
|
||||
this.props.collabStartSharingPointerEvent(pageId);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: { registered: boolean }) {
|
||||
|
|
@ -88,22 +102,30 @@ class Editor extends Component<Props> {
|
|||
this.props.isEditorInitializeError ||
|
||||
nextProps.creatingOnboardingDatabase !==
|
||||
this.props.creatingOnboardingDatabase ||
|
||||
nextState.registered !== this.state.registered
|
||||
nextState.registered !== this.state.registered ||
|
||||
(nextProps.isPageLevelSocketConnected &&
|
||||
!this.props.isPageLevelSocketConnected)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { pageId } = this.props.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.fetchPage(pageId);
|
||||
}
|
||||
|
||||
if (this.props.isPageLevelSocketConnected && isPageIdUpdated) {
|
||||
this.props.collabStartSharingPointerEvent(pageId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetEditorRequest();
|
||||
if (typeof this.unlisten === "function") this.unlisten();
|
||||
this.props.collabStopSharingPointerEvent();
|
||||
}
|
||||
|
||||
handleHistoryChange = (location: any) => {
|
||||
|
|
@ -143,6 +165,7 @@ class Editor extends Component<Props> {
|
|||
<AddCommentTourComponent />
|
||||
<CommentShowCaseCarousel />
|
||||
<GitSyncModal />
|
||||
<ConcurrentPageEditorToast />
|
||||
</GlobalHotKeys>
|
||||
</div>
|
||||
<ConfirmRunModal />
|
||||
|
|
@ -163,6 +186,7 @@ const mapStateToProps = (state: AppState) => ({
|
|||
user: getCurrentUser(state),
|
||||
creatingOnboardingDatabase: state.ui.onBoarding.showOnboardingLoader,
|
||||
currentApplicationName: state.ui.applications.currentApplication?.name,
|
||||
isPageLevelSocketConnected: getIsPageLevelSocketConnected(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
|
|
@ -174,6 +198,10 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||
dispatch(handlePathUpdated(location)),
|
||||
fetchPage: (pageId: string) => dispatch(fetchPage(pageId)),
|
||||
updateCurrentPage: (pageId: string) => dispatch(updateCurrentPage(pageId)),
|
||||
collabStartSharingPointerEvent: (pageId: string) =>
|
||||
dispatch(collabStartSharingPointerEvent(pageId)),
|
||||
collabStopSharingPointerEvent: () =>
|
||||
dispatch(collabStopSharingPointerEvent()),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { getDependenciesFromInverseDependencies } from "components/editorCompone
|
|||
import _, { debounce } from "lodash";
|
||||
import ReactDOM from "react-dom";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import getFeatureFlags from "utils/featureFlags";
|
||||
|
||||
export const draggableElement = (
|
||||
id: string,
|
||||
|
|
@ -41,7 +40,7 @@ export const draggableElement = (
|
|||
calculatedLeft: number,
|
||||
calculatedTop: number,
|
||||
) => {
|
||||
const bottomBarOffset = getFeatureFlags().GIT ? 34 : 0;
|
||||
const bottomBarOffset = 34;
|
||||
|
||||
if (calculatedLeft <= 0) {
|
||||
calculatedLeft = 0;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { cloneDeep } from "lodash";
|
|||
const initialState: AppCollabReducerState = {
|
||||
editors: [],
|
||||
pointerData: {},
|
||||
pageEditors: [],
|
||||
};
|
||||
|
||||
const appCollabReducer = createReducer(initialState, {
|
||||
|
|
@ -51,6 +52,13 @@ const appCollabReducer = createReducer(initialState, {
|
|||
pointerData: {},
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.APP_COLLAB_SET_CONCURRENT_PAGE_EDITORS]: (
|
||||
state: AppCollabReducerState,
|
||||
action: ReduxAction<any>,
|
||||
) => ({
|
||||
...state,
|
||||
pageEditors: action.payload,
|
||||
}),
|
||||
});
|
||||
|
||||
type PointerDataType = {
|
||||
|
|
@ -60,6 +68,7 @@ type PointerDataType = {
|
|||
export type AppCollabReducerState = {
|
||||
editors: User[];
|
||||
pointerData: PointerDataType;
|
||||
pageEditors: User[];
|
||||
};
|
||||
|
||||
export default appCollabReducer;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import { put, select } from "redux-saga/effects";
|
||||
import {
|
||||
APP_LEVEL_SOCKET_EVENTS,
|
||||
PAGE_LEVEL_SOCKET_EVENTS,
|
||||
} from "./socketEvents";
|
||||
import { APP_LEVEL_SOCKET_EVENTS } from "./socketEvents";
|
||||
|
||||
import {
|
||||
newCommentEvent,
|
||||
|
|
@ -96,7 +93,7 @@ export default function* handleAppLevelSocketEvents(event: any) {
|
|||
return;
|
||||
}
|
||||
// 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]));
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { PAGE_LEVEL_SOCKET_EVENTS } from "./socketEvents";
|
|||
import {
|
||||
collabSetEditorsPointersData,
|
||||
collabUnsetEditorsPointersData,
|
||||
collabConcurrentPageEditorsData,
|
||||
} from "actions/appCollabActions";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
|
|
@ -19,6 +20,11 @@ export default function* handlePageLevelSocketEvents(event: any, socket: any) {
|
|||
yield put(collabUnsetEditorsPointersData(event.payload[0]));
|
||||
return;
|
||||
}
|
||||
|
||||
case PAGE_LEVEL_SOCKET_EVENTS.LIST_ONLINE_PAGE_EDITORS: {
|
||||
yield put(collabConcurrentPageEditorsData(event.payload[0]?.users));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ export const APP_LEVEL_SOCKET_EVENTS = {
|
|||
|
||||
// notification events
|
||||
INSERT_NOTIFICATION: "insert:notification",
|
||||
|
||||
LIST_ONLINE_APP_EDITORS: "collab:online_editors", // user presence
|
||||
};
|
||||
|
||||
export const PAGE_LEVEL_SOCKET_EVENTS = {
|
||||
START_EDITING_APP: "collab:start_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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { AppState } from "reducers";
|
|||
import { AppCollabReducerState } from "reducers/uiReducers/appCollabReducer";
|
||||
import { getCurrentUser } from "./usersSelectors";
|
||||
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;
|
||||
|
||||
|
|
@ -14,3 +16,22 @@ export const getRealtimeAppEditors = createSelector(
|
|||
);
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const STORAGE_KEYS: { [id: string]: string } = {
|
|||
"FIRST_TIME_USER_ONBOARDING_APPLICATION_ID",
|
||||
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({
|
||||
|
|
@ -261,3 +262,32 @@ export const getFirstTimeUserOnboardingIntroModalVisibility = async () => {
|
|||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user