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("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);

View File

@ -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,
});

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 &
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}>
&nbsp;{props.actionElement}
{!props.hideActionElementSpace ? <>&nbsp;</> : ""}
{props.actionElement}
</StyledActionText>
)}
{props.variant === Variant.danger && props.showDebugButton ? (

View File

@ -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);
};

View File

@ -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}

View File

@ -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);

View File

@ -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",

View File

@ -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() {

View File

@ -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 />
</>
);
}

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 { 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(

View File

@ -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) {

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 { 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&apos;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) => (

View File

@ -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()),
};
};

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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);

View File

@ -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
};

View File

@ -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,
);
},
);

View File

@ -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);
}
};