From 3be937c503cd6c2c3b106e1cd8c9f78160b2df77 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Wed, 18 Oct 2023 10:14:13 +0100 Subject: [PATCH] Migrate layout context to TypeScript (#15275) Migrate layout context to TypeScript GitOrigin-RevId: ce453bfb67f7c36176fa24144413b556cd3c117e --- .../js/features/chat/components/chat-pane.jsx | 2 +- .../js/features/chat/context/chat-context.jsx | 2 +- .../settings/settings-overall-theme.tsx | 4 +- .../editor-navigation-toolbar-root.jsx | 13 +- .../components/layout-dropdown-button.jsx | 12 +- .../contexts/file-tree-selectable.jsx | 7 +- .../ide-react/context/connection-context.tsx | 4 +- .../components/detach-synctex-control.jsx | 16 +-- ...{layout-context.jsx => layout-context.tsx} | 118 +++++++++--------- .../pdf-preview/pdf-preview.spec.tsx | 11 +- 10 files changed, 80 insertions(+), 109 deletions(-) rename services/web/frontend/js/shared/context/{layout-context.jsx => layout-context.tsx} (62%) diff --git a/services/web/frontend/js/features/chat/components/chat-pane.jsx b/services/web/frontend/js/features/chat/components/chat-pane.jsx index 040a847f33..6cb55a1356 100644 --- a/services/web/frontend/js/features/chat/components/chat-pane.jsx +++ b/services/web/frontend/js/features/chat/components/chat-pane.jsx @@ -18,7 +18,7 @@ const MessageList = lazy(() => import('./message-list')) const ChatPane = React.memo(function ChatPane() { const { t } = useTranslation() - const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool }) + const { chatIsOpen } = useLayoutContext() const user = useUserContext({ id: PropTypes.string.isRequired, }) diff --git a/services/web/frontend/js/features/chat/context/chat-context.jsx b/services/web/frontend/js/features/chat/context/chat-context.jsx index 99429afbf2..cc6e744c24 100644 --- a/services/web/frontend/js/features/chat/context/chat-context.jsx +++ b/services/web/frontend/js/features/chat/context/chat-context.jsx @@ -136,7 +136,7 @@ export function ChatProvider({ children }) { _id: PropTypes.string.isRequired, }) - const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool }) + const { chatIsOpen } = useLayoutContext() const { hasFocus: windowHasFocus, diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-overall-theme.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-overall-theme.tsx index 9cd8087c1d..48392dac6e 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-overall-theme.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-overall-theme.tsx @@ -12,9 +12,7 @@ export default function SettingsOverallTheme() { const overallThemes = getMeta('ol-overallThemes') as | OverallThemeMeta[] | undefined - const { loadingStyleSheet } = useLayoutContext() as { - loadingStyleSheet: boolean - } + const { loadingStyleSheet } = useLayoutContext() const { overallTheme, setOverallTheme } = useProjectSettingsContext() const options: Array> = useMemo( diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.jsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.jsx index df73cc8850..fe3b28a07d 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.jsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.jsx @@ -23,17 +23,6 @@ const editorContextPropTypes = { permissionsLevel: PropTypes.string, } -const layoutContextPropTypes = { - chatIsOpen: PropTypes.bool, - setChatIsOpen: PropTypes.func.isRequired, - reviewPanelOpen: PropTypes.bool, - setReviewPanelOpen: PropTypes.func.isRequired, - view: PropTypes.string, - setView: PropTypes.func.isRequired, - setLeftMenuShown: PropTypes.func.isRequired, - pdfLayout: PropTypes.string.isRequired, -} - const chatContextPropTypes = { markMessagesAsRead: PropTypes.func.isRequired, unreadMessageCount: PropTypes.number.isRequired, @@ -70,7 +59,7 @@ const EditorNavigationToolbarRoot = React.memo( view, setView, setLeftMenuShown, - } = useLayoutContext(layoutContextPropTypes) + } = useLayoutContext() const { markMessagesAsRead, unreadMessageCount } = useChatContext(chatContextPropTypes) diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.jsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.jsx index eaba1dc7b6..1e24a50ae8 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.jsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button.jsx @@ -107,7 +107,7 @@ function LayoutDropdownButton() { changeLayout, view, pdfLayout, - } = useLayoutContext(layoutContextPropTypes) + } = useLayoutContext() const handleDetach = useCallback(() => { detach() @@ -250,13 +250,3 @@ IconCheckmark.propTypes = { view: PropTypes.string, detachRole: PropTypes.string, } - -const layoutContextPropTypes = { - reattach: PropTypes.func.isRequired, - detach: PropTypes.func.isRequired, - changeLayout: PropTypes.func.isRequired, - detachIsLinked: PropTypes.bool, - detachRole: PropTypes.string, - pdfLayout: PropTypes.string.isRequired, - view: PropTypes.string, -} diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.jsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.jsx index 0f2de76b30..b9a4b75a8d 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.jsx +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.jsx @@ -211,7 +211,7 @@ const editorContextPropTypes = { const isMac = /Mac/.test(window.navigator?.platform) export function useSelectableEntity(id, isFile) { - const { view, setView } = useLayoutContext(layoutContextPropTypes) + const { view, setView } = useLayoutContext() const { setContextMenuCoords } = useFileTreeMainContext() const { selectedEntityIds, @@ -288,11 +288,6 @@ export function useSelectableEntity(id, isFile) { return { isSelected, props } } -const layoutContextPropTypes = { - view: PropTypes.string, - setView: PropTypes.func.isRequired, -} - export function useFileTreeSelectable() { const context = useContext(FileTreeSelectableContext) diff --git a/services/web/frontend/js/features/ide-react/context/connection-context.tsx b/services/web/frontend/js/features/ide-react/context/connection-context.tsx index a6b70fcd3b..c39d982ed9 100644 --- a/services/web/frontend/js/features/ide-react/context/connection-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/connection-context.tsx @@ -22,7 +22,9 @@ type ConnectionContextValue = { registerUserActivity: () => void } -const ConnectionContext = createContext(null) +const ConnectionContext = createContext( + undefined +) export const ConnectionProvider: FC = ({ children }) => { const [connectionManager] = useState(() => new ConnectionManager()) diff --git a/services/web/frontend/js/features/pdf-preview/components/detach-synctex-control.jsx b/services/web/frontend/js/features/pdf-preview/components/detach-synctex-control.jsx index 812b342e39..e7fba2b8d3 100644 --- a/services/web/frontend/js/features/pdf-preview/components/detach-synctex-control.jsx +++ b/services/web/frontend/js/features/pdf-preview/components/detach-synctex-control.jsx @@ -1,9 +1,8 @@ -import PropTypes from 'prop-types' import { useLayoutContext } from '../../../shared/context/layout-context' import PdfSynctexControls from './pdf-synctex-controls' export function DefaultSynctexControl() { - const { detachRole } = useLayoutContext(layoutContextPropTypes) + const { detachRole } = useLayoutContext() if (!detachRole) { return } @@ -11,9 +10,7 @@ export function DefaultSynctexControl() { } export function DetacherSynctexControl() { - const { detachRole, detachIsLinked } = useLayoutContext( - layoutContextPropTypes - ) + const { detachRole, detachIsLinked } = useLayoutContext() if (detachRole === 'detacher' && detachIsLinked) { return } @@ -21,16 +18,9 @@ export function DetacherSynctexControl() { } export function DetachedSynctexControl() { - const { detachRole, detachIsLinked } = useLayoutContext( - layoutContextPropTypes - ) + const { detachRole, detachIsLinked } = useLayoutContext() if (detachRole === 'detached' && detachIsLinked) { return } return null } - -const layoutContextPropTypes = { - detachRole: PropTypes.string, - detachIsLinked: PropTypes.bool, -} diff --git a/services/web/frontend/js/shared/context/layout-context.jsx b/services/web/frontend/js/shared/context/layout-context.tsx similarity index 62% rename from services/web/frontend/js/shared/context/layout-context.jsx rename to services/web/frontend/js/shared/context/layout-context.tsx index d876fe4c4f..efb7a7906c 100644 --- a/services/web/frontend/js/shared/context/layout-context.jsx +++ b/services/web/frontend/js/shared/context/layout-context.tsx @@ -4,104 +4,108 @@ import { useCallback, useMemo, useEffect, + Dispatch, + SetStateAction, + FC, } from 'react' -import PropTypes from 'prop-types' import useScopeValue from '../hooks/use-scope-value' import useDetachLayout from '../hooks/use-detach-layout' -import { useIdeContext } from './ide-context' import localStorage from '../../infrastructure/local-storage' import getMeta from '../../utils/meta' import { debugConsole } from '@/utils/debugging' +import { BinaryFile } from '@/features/file-view/types/binary-file' + +export type IdeLayout = 'sideBySide' | 'flat' +export type IdeView = 'editor' | 'file' | 'pdf' | 'history' + +type LayoutContextValue = { + reattach: () => void + detach: () => void + detachIsLinked: boolean + detachRole: 'detacher' | 'detached' | null + changeLayout: (newLayout: IdeLayout, newView?: IdeView) => void + view: IdeView + setView: (view: IdeView) => void + chatIsOpen: boolean + setChatIsOpen: Dispatch> + reviewPanelOpen: boolean + setReviewPanelOpen: Dispatch< + SetStateAction + > + leftMenuShown: boolean + setLeftMenuShown: Dispatch< + SetStateAction + > + loadingStyleSheet: boolean + setLoadingStyleSheet: Dispatch< + SetStateAction + > + pdfLayout: IdeLayout + pdfPreviewOpen: boolean +} const debugPdfDetach = getMeta('ol-debugPdfDetach') -export const LayoutContext = createContext() +export const LayoutContext = createContext( + undefined +) -LayoutContext.Provider.propTypes = { - value: PropTypes.shape({ - reattach: PropTypes.func.isRequired, - detach: PropTypes.func.isRequired, - detachIsLinked: PropTypes.bool, - detachRole: PropTypes.string, - changeLayout: PropTypes.func.isRequired, - view: PropTypes.oneOf(['editor', 'file', 'pdf', 'history']), - setView: PropTypes.func.isRequired, - chatIsOpen: PropTypes.bool, - setChatIsOpen: PropTypes.func.isRequired, - reviewPanelOpen: PropTypes.bool, - setReviewPanelOpen: PropTypes.func.isRequired, - leftMenuShown: PropTypes.bool, - setLeftMenuShown: PropTypes.func.isRequired, - pdfLayout: PropTypes.oneOf(['sideBySide', 'flat']).isRequired, - pdfPreviewOpen: PropTypes.bool, - }).isRequired, -} - -function setLayoutInLocalStorage(pdfLayout) { +function setLayoutInLocalStorage(pdfLayout: IdeLayout) { localStorage.setItem( 'pdf.layout', pdfLayout === 'sideBySide' ? 'split' : 'flat' ) } -export function LayoutProvider({ children }) { - const { $scope } = useIdeContext() - +export const LayoutProvider: FC = ({ children }) => { // what to show in the "flat" view (editor or pdf) - const [view, _setView] = useScopeValue('ui.view') + const [view, _setView] = useScopeValue('ui.view') + const [toggleHistory] = useScopeValue<() => void>('toggleHistory') + const [openFile] = useScopeValue('openFile') const setView = useCallback( - value => { + (value: IdeView) => { _setView(oldValue => { // ensure that the "history:toggle" event is broadcast when switching in or out of history view if (value === 'history' || oldValue === 'history') { - $scope.toggleHistory() + toggleHistory() } - if (value === 'editor' && $scope.openFile) { + if (value === 'editor' && openFile) { // if a file is currently opened, ensure the view is 'file' instead of // 'editor' when the 'editor' view is requested. This is to ensure // that the entity selected in the file tree is the one visible and - // that docs don't take precendence over files. + // that docs don't take precedence over files. return 'file' } return value }) }, - [$scope, _setView] + [_setView, openFile, toggleHistory] ) // whether the chat pane is open - const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen') + const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen') // whether the review pane is open const [reviewPanelOpen, setReviewPanelOpen] = useScopeValue('ui.reviewPanelOpen') // whether the menu pane is open - const [leftMenuShown, setLeftMenuShown] = useScopeValue('ui.leftMenuShown') + const [leftMenuShown, setLeftMenuShown] = + useScopeValue('ui.leftMenuShown') // whether to display the editor and preview side-by-side or full-width ("flat") - const [pdfLayout, setPdfLayout] = useScopeValue('ui.pdfLayout') + const [pdfLayout, setPdfLayout] = useScopeValue('ui.pdfLayout') // whether stylesheet on theme is loading - const [loadingStyleSheet, setLoadingStyleSheet] = useScopeValue( + const [loadingStyleSheet, setLoadingStyleSheet] = useScopeValue( 'ui.loadingStyleSheet' ) - // switch to either side-by-side or flat (full-width) layout - const switchLayout = useCallback(() => { - setPdfLayout(layout => { - const newLayout = layout === 'sideBySide' ? 'flat' : 'sideBySide' - setView(newLayout === 'sideBySide' ? 'editor' : 'pdf') - setPdfLayout(newLayout) - setLayoutInLocalStorage(newLayout) - }) - }, [setPdfLayout, setView]) - const changeLayout = useCallback( - (newLayout, newView) => { + (newLayout: IdeLayout, newView: IdeView = 'editor') => { setPdfLayout(newLayout) setView(newLayout === 'sideBySide' ? 'editor' : newView) setLayoutInLocalStorage(newLayout) @@ -151,7 +155,7 @@ export function LayoutProvider({ children }) { changeLayout, ]) - const value = useMemo( + const value = useMemo( () => ({ reattach, detach, @@ -170,7 +174,6 @@ export function LayoutProvider({ children }) { setReviewPanelOpen, setLoadingStyleSheet, setView, - switchLayout, view, }), [ @@ -191,7 +194,6 @@ export function LayoutProvider({ children }) { setReviewPanelOpen, setLoadingStyleSheet, setView, - switchLayout, view, ] ) @@ -201,12 +203,10 @@ export function LayoutProvider({ children }) { ) } -LayoutProvider.propTypes = { - children: PropTypes.any, -} - -export function useLayoutContext(propTypes) { - const data = useContext(LayoutContext) - PropTypes.checkPropTypes(propTypes, data, 'data', 'LayoutContext.Provider') - return data +export function useLayoutContext() { + const context = useContext(LayoutContext) + if (!context) { + throw new Error('useLayoutContext is only available inside LayoutProvider') + } + return context } diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx index dee093459d..eee66d556c 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx @@ -2,7 +2,11 @@ import localStorage from '../../../../frontend/js/infrastructure/local-storage' import PdfPreview from '../../../../frontend/js/features/pdf-preview/components/pdf-preview' import { EditorProviders } from '../../helpers/editor-providers' import { mockScope } from './scope' -import { useLayoutContext } from '../../../../frontend/js/shared/context/layout-context' +import { + IdeLayout, + IdeView, + useLayoutContext, +} from '../../../../frontend/js/shared/context/layout-context' import { FC, useEffect } from 'react' const storeAndFireEvent = (win: typeof window, key: string, value: unknown) => { @@ -10,7 +14,10 @@ const storeAndFireEvent = (win: typeof window, key: string, value: unknown) => { win.dispatchEvent(new StorageEvent('storage', { key })) } -const Layout: FC<{ layout: string; view?: string }> = ({ layout, view }) => { +const Layout: FC<{ layout: IdeLayout; view?: IdeView }> = ({ + layout, + view, +}) => { const { changeLayout } = useLayoutContext() useEffect(() => {