diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Unique_key.spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Unique_key.spec.js index 85755ff5df..505d2870a1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Unique_key.spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Canvas/Unique_key.spec.js @@ -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); diff --git a/app/client/src/actions/appCollabActions.ts b/app/client/src/actions/appCollabActions.ts index 43220c461c..cc8c7cd289 100644 --- a/app/client/src/actions/appCollabActions.ts +++ b/app/client/src/actions/appCollabActions.ts @@ -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, +}); diff --git a/app/client/src/comments/ConcurrentPageEditorToast.tsx b/app/client/src/comments/ConcurrentPageEditorToast.tsx new file mode 100644 index 0000000000..5a41e04165 --- /dev/null +++ b/app/client/src/comments/ConcurrentPageEditorToast.tsx @@ -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 ( + + {showToast && ( + Dismiss + } + contentClassName="concurrent-editing-warning-text " + hideActionElementSpace + text={getMessage()} + width={"327px"} + /> + )} + {showToast && } + + ); +} diff --git a/app/client/src/components/ads/Toast.tsx b/app/client/src/components/ads/Toast.tsx index 66244f8050..928b607a58 100644 --- a/app/client/src/components/ads/Toast.tsx +++ b/app/client/src/components/ads/Toast.tsx @@ -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} > - + {props.variant === Variant.success ? ( ) : props.variant === Variant.warning ? ( @@ -145,10 +153,13 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) { ) : null} - {props.text} + + {props.text} + {props.actionElement && ( -  {props.actionElement} + {!props.hideActionElementSpace ? <>  : ""} + {props.actionElement} )} {props.variant === Variant.danger && props.showDebugButton ? ( diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 9eaadfc8fe..fafbc942bb 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -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 = 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); }; diff --git a/app/client/src/components/editorComponents/Debugger/index.tsx b/app/client/src/components/editorComponents/Debugger/index.tsx index 95abf17fae..d212aa130f 100644 --- a/app/client/src/components/editorComponents/Debugger/index.tsx +++ b/app/client/src/components/editorComponents/Debugger/index.tsx @@ -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 ( - - - {!!messageCounters.errors && ( -
- {totalMessageCount} -
- )} -
- ); return showDebugger ? ( ) : 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 ( - + + + {!!messageCounters.errors && (
{totalMessageCount > 9 ? "9+" : totalMessageCount} diff --git a/app/client/src/constants/Layers.tsx b/app/client/src/constants/Layers.tsx index 61d4ce402b..5cccb2f860 100644 --- a/app/client/src/constants/Layers.tsx +++ b/app/client/src/constants/Layers.tsx @@ -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); diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 1164bef5fc..5fe8162849 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -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", diff --git a/app/client/src/pages/Editor/BottomBar/index.tsx b/app/client/src/pages/Editor/BottomBar/index.tsx index 0cde89d240..03e7252e3b 100644 --- a/app/client/src/pages/Editor/BottomBar/index.tsx +++ b/app/client/src/pages/Editor/BottomBar/index.tsx @@ -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() { diff --git a/app/client/src/pages/Editor/MainContainer.tsx b/app/client/src/pages/Editor/MainContainer.tsx index 5a2d746e4f..d61d6c2609 100644 --- a/app/client/src/pages/Editor/MainContainer.tsx +++ b/app/client/src/pages/Editor/MainContainer.tsx @@ -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() { - {getFeatureFlags().GIT && } + ); } diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index 66f4cfc421..8ea5edf87f 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -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( diff --git a/app/client/src/pages/Editor/RealtimeAppEditors.tsx b/app/client/src/pages/Editor/RealtimeAppEditors.tsx index 6b652afb10..b63e5baea5 100644 --- a/app/client/src/pages/Editor/RealtimeAppEditors.tsx +++ b/app/client/src/pages/Editor/RealtimeAppEditors.tsx @@ -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) { diff --git a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx index 5e7f3e9569..206bb8356a 100644 --- a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx +++ b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx @@ -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 ( - -
@@ -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()), }; }; diff --git a/app/client/src/pages/Editor/utils.ts b/app/client/src/pages/Editor/utils.ts index e48d67ee66..986d847b50 100644 --- a/app/client/src/pages/Editor/utils.ts +++ b/app/client/src/pages/Editor/utils.ts @@ -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; diff --git a/app/client/src/reducers/uiReducers/appCollabReducer.ts b/app/client/src/reducers/uiReducers/appCollabReducer.ts index 2db02933d1..923f4ed7a7 100644 --- a/app/client/src/reducers/uiReducers/appCollabReducer.ts +++ b/app/client/src/reducers/uiReducers/appCollabReducer.ts @@ -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, + ) => ({ + ...state, + pageEditors: action.payload, + }), }); type PointerDataType = { @@ -60,6 +68,7 @@ type PointerDataType = { export type AppCollabReducerState = { editors: User[]; pointerData: PointerDataType; + pageEditors: User[]; }; export default appCollabReducer; diff --git a/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.ts b/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.ts index d73f271e4f..d46ca5e324 100644 --- a/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.ts +++ b/app/client/src/sagas/WebsocketSagas/handleAppLevelSocketEvents.ts @@ -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; } diff --git a/app/client/src/sagas/WebsocketSagas/handlePageLevelSocketEvents.ts b/app/client/src/sagas/WebsocketSagas/handlePageLevelSocketEvents.ts index 4f2eb53294..d6608c1055 100644 --- a/app/client/src/sagas/WebsocketSagas/handlePageLevelSocketEvents.ts +++ b/app/client/src/sagas/WebsocketSagas/handlePageLevelSocketEvents.ts @@ -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); diff --git a/app/client/src/sagas/WebsocketSagas/socketEvents.ts b/app/client/src/sagas/WebsocketSagas/socketEvents.ts index 07d65949fd..cdc892c16c 100644 --- a/app/client/src/sagas/WebsocketSagas/socketEvents.ts +++ b/app/client/src/sagas/WebsocketSagas/socketEvents.ts @@ -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 }; diff --git a/app/client/src/selectors/appCollabSelectors.tsx b/app/client/src/selectors/appCollabSelectors.tsx index 159d9374bd..cd81d899a9 100644 --- a/app/client/src/selectors/appCollabSelectors.tsx +++ b/app/client/src/selectors/appCollabSelectors.tsx @@ -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, + ); + }, +); diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index db3af9c0cb..dab530ab9d 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -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); + } +};