From e65d0c8073ef955756911bf0c105b8055093b9f7 Mon Sep 17 00:00:00 2001 From: Rohan Mathur Date: Thu, 22 Aug 2024 13:07:41 +0600 Subject: [PATCH] [ENGG-2162] feat: full screen mode for code editors (#2092) * add full screen toggle * fix: reset * fix: keyboard close * add onboarding toast * fix: style * chore: add analytics * fix: events props --- .../mocksV2/MockEditorIndex/Editor/index.tsx | 2 + .../Rows/RowsMarkup/CustomScriptRow/index.jsx | 2 + .../Rows/RowsMarkup/RequestBodyRow/index.js | 4 +- .../Rows/RowsMarkup/ResponseBodyRow/index.js | 4 +- .../TrafficTableV2/FixedRequestLogPane.js | 1 + .../CodeEditor/components/Editor/Editor.tsx | 98 ++++++++++++++++++- .../Editor/components/Toolbar/Toolbar.tsx | 25 ++++- .../CodeEditor/components/Editor/editor.scss | 32 ++++++ .../components/analytics/constants.ts | 2 + .../CodeEditor/components/analytics/index.ts | 11 +++ .../componentsV2/CodeEditor/types/index.ts | 17 ++++ app/src/store/features/userActions.js | 4 + app/src/store/initial-state/index.js | 1 + app/src/store/selectors.js | 4 + .../client-view/request/RequestBody.tsx | 1 + .../client-view/response/ResponseBody.tsx | 10 +- 16 files changed, 211 insertions(+), 7 deletions(-) diff --git a/app/src/components/features/mocksV2/MockEditorIndex/Editor/index.tsx b/app/src/components/features/mocksV2/MockEditorIndex/Editor/index.tsx index 541b396bd2..acbdbc8887 100644 --- a/app/src/components/features/mocksV2/MockEditorIndex/Editor/index.tsx +++ b/app/src/components/features/mocksV2/MockEditorIndex/Editor/index.tsx @@ -331,6 +331,7 @@ const MockEditor: React.FC = ({ defaultValue={headersString} handleChange={setHeadersString} language={EditorLanguage.JSON} + analyticEventProperties={{ source: "mocks", mock_type: type }} /> @@ -349,6 +350,7 @@ const MockEditor: React.FC = ({ handleChange={setBody} language={getEditorLanguage(fileType)} isReadOnly={isEditorReadOnly} + analyticEventProperties={{ source: "mocks", mock_type: mockType }} toolbarOptions={{ title: mockType === MockType.FILE ? "File content" : "", options: null, diff --git a/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/CustomScriptRow/index.jsx b/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/CustomScriptRow/index.jsx index ee8afe6c3c..fe680dce1d 100644 --- a/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/CustomScriptRow/index.jsx +++ b/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/CustomScriptRow/index.jsx @@ -15,6 +15,7 @@ import { getDefaultScriptRender, createRenderedScript } from "./utils"; import { isExtensionManifestVersion3 } from "actions/ExtensionActions"; import { MockPickerModal } from "features/mocks/modals"; import CodeEditor, { EditorLanguage } from "componentsV2/CodeEditor"; +import { RuleType } from "features/rules"; const { Text } = Typography; @@ -246,6 +247,7 @@ const CustomScriptRow = ({ value={initialCodeEditorValue} handleChange={handleEditorUpdate} isReadOnly={isInputDisabled} + analyticEventProperties={{ source: "rule_editor", rule_type: RuleType.SCRIPT }} toolbarOptions={{ title: "Code", }} diff --git a/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/RequestBodyRow/index.js b/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/RequestBodyRow/index.js index 274c4f42ca..0d58ea4965 100644 --- a/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/RequestBodyRow/index.js +++ b/app/src/components/features/rules/RulePairs/Pairs/Rows/RowsMarkup/RequestBodyRow/index.js @@ -13,6 +13,7 @@ import { PremiumIcon } from "components/common/PremiumIcon"; import { PremiumFeature } from "features/pricing"; import CodeEditor, { EditorLanguage } from "componentsV2/CodeEditor"; import { MdInfoOutline } from "@react-icons/all-files/md/MdInfoOutline"; +import { RuleType } from "features/rules"; const RequestBodyRow = ({ rowIndex, pair, pairIndex, ruleDetails, isInputDisabled }) => { const dispatch = useDispatch(); @@ -188,7 +189,7 @@ const RequestBodyRow = ({ rowIndex, pair, pairIndex, ruleDetails, isInputDisable > { @@ -299,7 +300,7 @@ const ResponseBodyRow = ({ rowIndex, pair, pairIndex, ruleDetails, isInputDisabl > ), diff --git a/app/src/componentsV2/CodeEditor/components/Editor/Editor.tsx b/app/src/componentsV2/CodeEditor/components/Editor/Editor.tsx index 2fe884d28d..a86f3abeed 100644 --- a/app/src/componentsV2/CodeEditor/components/Editor/Editor.tsx +++ b/app/src/componentsV2/CodeEditor/components/Editor/Editor.tsx @@ -5,14 +5,19 @@ import { json } from "@codemirror/lang-json"; import { html } from "@codemirror/lang-html"; import { css } from "@codemirror/lang-css"; import { vscodeDark } from "@uiw/codemirror-theme-vscode"; -import { EditorLanguage, EditorCustomToolbar } from "componentsV2/CodeEditor/types"; +import { EditorLanguage, EditorCustomToolbar, AnalyticEventProperties } from "componentsV2/CodeEditor/types"; import { ResizableBox } from "react-resizable"; import { useDispatch, useSelector } from "react-redux"; import { actions } from "store"; -import { getAllEditorToast } from "store/selectors"; +import { getAllEditorToast, getIsCodeEditorFullScreenModeOnboardingCompleted } from "store/selectors"; import { EditorToastContainer } from "../EditorToast/EditorToastContainer"; import { getByteSize } from "utils/FormattingHelper"; import CodeEditorToolbar from "./components/Toolbar/Toolbar"; +import { Modal } from "antd"; +import { toast } from "utils/Toast"; +import { useLocation } from "react-router-dom"; +import PATHS from "config/constants/sub/paths"; +import { trackCodeEditorCollapsedClick, trackCodeEditorExpandedClick } from "../analytics"; import "./editor.scss"; interface EditorProps { @@ -26,6 +31,7 @@ interface EditorProps { toolbarOptions?: EditorCustomToolbar; hideCharacterCount?: boolean; handleChange?: (value: string) => void; + analyticEventProperties?: AnalyticEventProperties; } const Editor: React.FC = ({ @@ -39,10 +45,14 @@ const Editor: React.FC = ({ handleChange = () => {}, toolbarOptions, id = "", + analyticEventProperties = {}, }) => { + const location = useLocation(); const dispatch = useDispatch(); const [editorHeight, setEditorHeight] = useState(height); const [editorContent, setEditorContent] = useState(value); + const [isFullScreen, setIsFullScreen] = useState(false); + const isFullScreenModeOnboardingCompleted = useSelector(getIsCodeEditorFullScreenModeOnboardingCompleted); const allEditorToast = useSelector(getAllEditorToast); const toastOverlay = useMemo(() => allEditorToast[id], [allEditorToast, id]); // todo: rename @@ -51,6 +61,27 @@ const Editor: React.FC = ({ setEditorHeight(size.height); }; + const handleFullScreenToggle = () => { + setIsFullScreen((prev) => !prev); + + if (!isFullScreen) { + trackCodeEditorExpandedClick(analyticEventProperties); + + if (!isFullScreenModeOnboardingCompleted) { + // TODO: @rohanmathur to remove this check after adding shortcut in mocks save button + const isRuleEditor = location?.pathname.includes(PATHS.RULE_EDITOR.RELATIVE); + + if (isRuleEditor) { + toast.info(`Use '⌘+S' or 'ctrl+S' to save the rule`, 3); + // @ts-ignore + dispatch(actions.updateIsCodeEditorFullScreenModeOnboardingCompleted(true)); + } + } + } else { + trackCodeEditorCollapsedClick(analyticEventProperties); + } + }; + const editorLanguage = useMemo(() => { switch (language) { case EditorLanguage.JAVASCRIPT: @@ -91,14 +122,75 @@ const Editor: React.FC = ({ [handleChange] ); - return ( + return isFullScreen ? ( + <> + { + setIsFullScreen(false); + }} + closable={false} + closeIcon={null} + maskClosable={false} + wrapClassName="code-editor-fullscreen-modal" + maskStyle={{ background: "var(--requestly-color-surface-0, #212121)" }} + footer={
{getByteSize(value)} characters
} + > + { + setEditorContent(formattedCode); + }} + handleFullScreenToggle={handleFullScreenToggle} + customOptions={toolbarOptions} + /> + + <> + {toastOverlay && ( + handleEditorClose(toastOverlay.id)} + isVisible={toastOverlay} + autoClose={toastOverlay.autoClose} + /> + )} + + +
+ + ) : ( <> { setEditorContent(formattedCode); }} + handleFullScreenToggle={handleFullScreenToggle} customOptions={toolbarOptions} /> void; + isFullScreen: boolean; + handleFullScreenToggle: () => void; } -const CodeEditorToolbar: React.FC = ({ language, code, onCodeFormat, customOptions }) => { +const CodeEditorToolbar: React.FC = ({ + language, + code, + onCodeFormat, + customOptions, + isFullScreen = false, + handleFullScreenToggle = () => {}, +}) => { const theme = useTheme(); const [isCodePrettified, setIsCodePrettified] = useState(false); const [isCopied, setIsCopied] = useState(false); @@ -106,6 +117,18 @@ const CodeEditorToolbar: React.FC = ({ language, code, o onClick={handleCodeFormatting} /> + + + : } + onClick={handleFullScreenToggle} + /> + ); diff --git a/app/src/componentsV2/CodeEditor/components/Editor/editor.scss b/app/src/componentsV2/CodeEditor/components/Editor/editor.scss index a2491591a6..4a6351ce31 100644 --- a/app/src/componentsV2/CodeEditor/components/Editor/editor.scss +++ b/app/src/componentsV2/CodeEditor/components/Editor/editor.scss @@ -50,3 +50,35 @@ color: var(--requestly-color-text-subtle); font-size: var(--requestly-font-size-xs); } + +.code-editor-fullscreen-modal { + .ant-modal { + width: 100% !important; + max-width: 1440px; + top: 0px; + padding: 8px; + margin: 0 auto; + + .ant-modal-content { + border-radius: var(--space-4, 8px); + border: 1px solid var(--requestly-color-white-t-10, rgba(255, 255, 255, 0.06)); + + .ant-modal-body { + padding: 8px 8px 24px 8px !important; + height: calc(100vh - 54px) !important; // 54px -> vertical padding + footer height + + .code-editor { + border: none; + border-radius: 4px; + height: calc(100% - 20px); + background: var(--requestly-color-surface-0, #212121); + } + } + + .ant-modal-footer { + padding: 8px; + border: none; + } + } + } +} diff --git a/app/src/componentsV2/CodeEditor/components/analytics/constants.ts b/app/src/componentsV2/CodeEditor/components/analytics/constants.ts index 07116f4432..ccdf7290dd 100644 --- a/app/src/componentsV2/CodeEditor/components/analytics/constants.ts +++ b/app/src/componentsV2/CodeEditor/components/analytics/constants.ts @@ -2,4 +2,6 @@ export const CODE_EDITOR = { CODE_COPIED: "code_editor_code_copied", CODE_PRETTIFIED: "code_editor_code_prettified", CODE_MINIFIED: "code_editor_code_minified", + EXPANDED: "code_editor_expanded", + COLLAPSED: "code_editor_collapsed", }; diff --git a/app/src/componentsV2/CodeEditor/components/analytics/index.ts b/app/src/componentsV2/CodeEditor/components/analytics/index.ts index f33994ec56..b401776dea 100644 --- a/app/src/componentsV2/CodeEditor/components/analytics/index.ts +++ b/app/src/componentsV2/CodeEditor/components/analytics/index.ts @@ -1,5 +1,6 @@ import { trackEvent } from "modules/analytics"; import { CODE_EDITOR } from "./constants"; +import { AnalyticEventProperties } from "componentsV2/CodeEditor/types"; export const trackCodeEditorCodeCopied = () => { trackEvent(CODE_EDITOR.CODE_COPIED); @@ -12,3 +13,13 @@ export const trackCodeEditorCodePrettified = () => { export const trackCodeEditorCodeMinified = () => { trackEvent(CODE_EDITOR.CODE_MINIFIED); }; + +export const trackCodeEditorExpandedClick = (props: AnalyticEventProperties) => { + const params = { ...props }; + trackEvent(CODE_EDITOR.EXPANDED, params); +}; + +export const trackCodeEditorCollapsedClick = (props: AnalyticEventProperties) => { + const params = { ...props }; + trackEvent(CODE_EDITOR.COLLAPSED, params); +}; diff --git a/app/src/componentsV2/CodeEditor/types/index.ts b/app/src/componentsV2/CodeEditor/types/index.ts index b35a1b038b..e5a6b36548 100644 --- a/app/src/componentsV2/CodeEditor/types/index.ts +++ b/app/src/componentsV2/CodeEditor/types/index.ts @@ -1,4 +1,6 @@ +import { MockType } from "components/features/mocksV2/types"; import { ReactNode } from "react"; +import { RuleType } from "types"; export enum EditorLanguage { JAVASCRIPT = "javascript", @@ -29,3 +31,18 @@ export const getEditorLanguageFromContentType = (contentType: string): EditorLan return EditorLanguage.CSS; } }; + +export type AnalyticEventProperties = + | { + source: "rule_editor"; + rule_type: RuleType; + } + | { + source: "mocks"; + mock_type: MockType; + } + | { + source: "api_client"; + } + | { source: "traffic_table" } + | Record; diff --git a/app/src/store/features/userActions.js b/app/src/store/features/userActions.js index 65591c6ba1..b91c092c33 100644 --- a/app/src/store/features/userActions.js +++ b/app/src/store/features/userActions.js @@ -208,3 +208,7 @@ export const updateIsAppBannerVisible = (prevState, action) => { export const updateIsSupportChatOpened = (prevState, action) => { prevState.misc.persist.isSupportChatOpened = action.payload; }; + +export const updateIsCodeEditorFullScreenModeOnboardingCompleted = (prevState, action) => { + prevState.misc.persist.isCodeEditorFullScreenModeOnboardingCompleted = action.payload; +}; diff --git a/app/src/store/initial-state/index.js b/app/src/store/initial-state/index.js index a65a9c3e4d..0b440da3c1 100644 --- a/app/src/store/initial-state/index.js +++ b/app/src/store/initial-state/index.js @@ -195,6 +195,7 @@ const INITIAL_STATE = { isConnectedAppsTourCompleted: false, isNetworkSessionTooltipShown: false, isRuleEditorTourCompleted: false, + isCodeEditorFullScreenModeOnboardingCompleted: false, extensionInstallSource: null, isMiscTourCompleted: { firstDraftSession: false, diff --git a/app/src/store/selectors.js b/app/src/store/selectors.js index 27ea3cdd59..64ecb9e6bb 100644 --- a/app/src/store/selectors.js +++ b/app/src/store/selectors.js @@ -237,6 +237,10 @@ export const getIsRuleEditorTourCompleted = (state) => { ); }; +export const getIsCodeEditorFullScreenModeOnboardingCompleted = (state) => { + return getGlobalState(state).misc.persist?.isCodeEditorFullScreenModeOnboardingCompleted; +}; + export const getIsMiscTourCompleted = (state) => { return getGlobalState(state).misc.persist?.isMiscTourCompleted; }; diff --git a/app/src/views/features/api-client/client-view/request/RequestBody.tsx b/app/src/views/features/api-client/client-view/request/RequestBody.tsx index 7c7aef3d76..72f27e20a3 100644 --- a/app/src/views/features/api-client/client-view/request/RequestBody.tsx +++ b/app/src/views/features/api-client/client-view/request/RequestBody.tsx @@ -23,6 +23,7 @@ const RequestBody: React.FC = ({ body, contentType, setBody, setContentTy handleChange={setBody} isResizable={false} hideCharacterCount + analyticEventProperties={{ source: "api_client" }} /> ); diff --git a/app/src/views/features/api-client/client-view/response/ResponseBody.tsx b/app/src/views/features/api-client/client-view/response/ResponseBody.tsx index 961270ab06..21e5c6924c 100644 --- a/app/src/views/features/api-client/client-view/response/ResponseBody.tsx +++ b/app/src/views/features/api-client/client-view/response/ResponseBody.tsx @@ -37,7 +37,15 @@ const ResponseBody: React.FC = ({ responseText, contentTypeHeader }) => { const editorLanguage = getEditorLanguageFromContentType(contentTypeHeader); if (editorLanguage) { - return ; + return ( + + ); } return null;