From 72f03286335839a82b247c5edc27fcaead8af44d Mon Sep 17 00:00:00 2001 From: John Joyce Date: Fri, 20 Jan 2023 09:33:44 -0800 Subject: [PATCH] feat(ui): Documentation Editor Improvements (#7072) --- .../src/app/context/UserContextProvider.tsx | 6 ++ .../src/app/context/useInitialRedirect.ts | 51 ++++++++++++ .../src/app/context/userContext.tsx | 7 ++ .../tabs/Documentation/DocumentationTab.tsx | 35 +++++++- .../components/DescriptionEditor.tsx | 83 +++++++++---------- .../components/DescriptionEditorToolbar.tsx | 23 +++++ .../components/DescriptionPreview.tsx | 25 ++++++ .../components/DescriptionPreviewModal.tsx | 76 +++++++++++++++++ .../components/DescriptionPreviewToolbar.tsx | 18 ++++ .../components/DiscardDescriptionModal.tsx | 6 +- 10 files changed, 279 insertions(+), 51 deletions(-) create mode 100644 datahub-web-react/src/app/context/useInitialRedirect.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditorToolbar.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreview.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewModal.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewToolbar.tsx diff --git a/datahub-web-react/src/app/context/UserContextProvider.tsx b/datahub-web-react/src/app/context/UserContextProvider.tsx index 3bcff15cc2748..4a9f24cdea30e 100644 --- a/datahub-web-react/src/app/context/UserContextProvider.tsx +++ b/datahub-web-react/src/app/context/UserContextProvider.tsx @@ -3,6 +3,7 @@ import { useGetMeLazyQuery } from '../../graphql/me.generated'; import { useGetGlobalViewsSettingsLazyQuery } from '../../graphql/app.generated'; import { CorpUser, PlatformPrivileges } from '../../types.generated'; import { UserContext, LocalState, DEFAULT_STATE, State } from './userContext'; +import { useInitialRedirect } from './useInitialRedirect'; // TODO: Migrate all usage of useAuthenticatedUser to using this provider. @@ -124,6 +125,11 @@ const UserContextProvider = ({ children }: { children: React.ReactNode }) => { } }, [state, localState.selectedViewUrn, setDefaultSelectedView]); + /** + * Route to the most recently visited path once on first load of home page, if present in local storage. + */ + useInitialRedirect(state, localState, setState, updateLocalState); + return ( { + if ( + location.pathname === PageRoutes.ROOT && + !state.loadedInitialPath && + localState.selectedPath !== location.pathname + ) { + setState({ + ...state, + loadedInitialPath: true, + }); + if (localState.selectedPath) { + history.push({ + pathname: localState.selectedPath, + search: localState.selectedSearch || '', + }); + } + } + }, [ + localState.selectedPath, + localState.selectedSearch, + location.pathname, + location.search, + state, + history, + setState, + ]); + + /** + * When the location of the browse changes, save the latest to local state. + */ + useEffect(() => { + if (localState.selectedPath !== location.pathname || localState.selectedSearch !== location.search) { + setLocalState({ + ...localState, + selectedPath: location.pathname, + selectedSearch: location.search, + }); + } + }, [location.pathname, location.search, localState, setLocalState]); +} diff --git a/datahub-web-react/src/app/context/userContext.tsx b/datahub-web-react/src/app/context/userContext.tsx index ebd62d3a1fea5..c5522346483dd 100644 --- a/datahub-web-react/src/app/context/userContext.tsx +++ b/datahub-web-react/src/app/context/userContext.tsx @@ -6,6 +6,8 @@ import { CorpUser, PlatformPrivileges } from '../../types.generated'; */ export type LocalState = { selectedViewUrn?: string | null; + selectedPath?: string | null; + selectedSearch?: string | null; }; /** @@ -19,6 +21,10 @@ export type State = { loadedPersonalDefaultViewUrn: boolean; hasSetDefaultView: boolean; }; + /** + * Whether the initial page path has been loaded. + */ + loadedInitialPath: boolean; }; /** @@ -47,6 +53,7 @@ export const DEFAULT_STATE: State = { loadedPersonalDefaultViewUrn: false, hasSetDefaultView: false, }, + loadedInitialPath: false, }; export const DEFAULT_CONTEXT = { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/DocumentationTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/DocumentationTab.tsx index f5563eae7fade..de065d23e56e7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Documentation/DocumentationTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/DocumentationTab.tsx @@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; import { Button, Divider, Typography } from 'antd'; -import { EditOutlined } from '@ant-design/icons'; +import { EditOutlined, ExpandAltOutlined } from '@ant-design/icons'; import TabToolbar from '../../components/styled/TabToolbar'; import { AddLinkModal } from '../../components/styled/AddLinkModal'; @@ -15,6 +15,7 @@ import { LinkList } from './components/LinkList'; import { useEntityData, useRefetch, useRouteToTab } from '../../EntityContext'; import { EDITED_DESCRIPTIONS_CACHE_NAME } from '../../utils'; import { Editor } from './components/editor/Editor'; +import { DescriptionPreviewModal } from './components/DescriptionPreviewModal'; const DocumentationContainer = styled.div` margin: 0 32px; @@ -36,15 +37,19 @@ export const DocumentationTab = ({ properties }: { properties?: Props }) => { const routeToTab = useRouteToTab(); const isEditing = queryString.parse(useLocation().search, { parseBooleans: true }).editing; + const showModal = queryString.parse(useLocation().search, { parseBooleans: true }).modal; useEffect(() => { const editedDescriptions = (localStorageDictionary && JSON.parse(localStorageDictionary)) || {}; if (editedDescriptions.hasOwnProperty(urn)) { - routeToTab({ tabName: 'Documentation', tabParams: { editing: true } }); + routeToTab({ + tabName: 'Documentation', + tabParams: { editing: true, modal: !!showModal }, + }); } - }, [urn, routeToTab, localStorageDictionary]); + }, [urn, routeToTab, showModal, localStorageDictionary]); - return isEditing ? ( + return isEditing && !showModal ? ( <> routeToTab({ tabName: 'Documentation' })} /> @@ -62,6 +67,19 @@ export const DocumentationTab = ({ properties }: { properties?: Props }) => { {!hideLinksButton && } +
+ +
{description ? ( @@ -85,6 +103,15 @@ export const DocumentationTab = ({ properties }: { properties?: Props }) => { {!hideLinksButton && } )} + {showModal && ( + { + routeToTab({ tabName: 'Documentation', tabParams: { editing: false } }); + }} + /> + )} ); }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditor.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditor.tsx index a73a93230d870..a0faa348d35a1 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditor.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditor.tsx @@ -1,17 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { message, Button } from 'antd'; -import { CheckOutlined } from '@ant-design/icons'; +import { message } from 'antd'; import styled from 'styled-components'; - import analytics, { EventType, EntityActionType } from '../../../../../analytics'; - -import TabToolbar from '../../../components/styled/TabToolbar'; - import { GenericEntityUpdate } from '../../../types'; import { useEntityData, useEntityUpdate, useMutationUrn, useRefetch } from '../../../EntityContext'; import { useUpdateDescriptionMutation } from '../../../../../../graphql/mutations.generated'; import { DiscardDescriptionModal } from './DiscardDescriptionModal'; import { EDITED_DESCRIPTIONS_CACHE_NAME } from '../../../utils'; +import { DescriptionEditorToolbar } from './DescriptionEditorToolbar'; import { Editor } from './editor/Editor'; const EditorContainer = styled.div` @@ -19,7 +15,11 @@ const EditorContainer = styled.div` height: 100%; `; -export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) => { +type DescriptionEditorProps = { + onComplete?: () => void; +}; + +export const DescriptionEditor = ({ onComplete }: DescriptionEditorProps) => { const mutationUrn = useMutationUrn(); const { entityType, entityData } = useEntityData(); const refetch = useRefetch(); @@ -34,7 +34,23 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) = const [updatedDescription, setUpdatedDescription] = useState(description); const [isDescriptionUpdated, setIsDescriptionUpdated] = useState(editedDescriptions.hasOwnProperty(mutationUrn)); - const [cancelModalVisible, setCancelModalVisible] = useState(false); + const [confirmCloseModalVisible, setConfirmCloseModalVisible] = useState(false); + + /** + * Auto-Save the description edits to local storage every 5 seconds. + */ + useEffect(() => { + let delayDebounceFn: ReturnType; + const editedDescriptionsLocal = (localStorageDictionary && JSON.parse(localStorageDictionary)) || {}; + + if (isDescriptionUpdated) { + delayDebounceFn = setTimeout(() => { + editedDescriptionsLocal[mutationUrn] = updatedDescription; + localStorage.setItem(EDITED_DESCRIPTIONS_CACHE_NAME, JSON.stringify(editedDescriptionsLocal)); + }, 5000); + } + return () => clearTimeout(delayDebounceFn); + }, [mutationUrn, isDescriptionUpdated, updatedDescription, localStorageDictionary]); const updateDescriptionLegacy = () => { return updateEntity?.({ @@ -53,7 +69,7 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) = }); }; - const handleSaveDescription = async () => { + const handleSave = async () => { message.loading({ content: 'Saving...' }); try { if (updateEntity) { @@ -98,32 +114,14 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) = } }; - // Updating the localStorage when the user has paused for 5 sec - useEffect(() => { - let delayDebounceFn: ReturnType; - const editedDescriptionsLocal = (localStorageDictionary && JSON.parse(localStorageDictionary)) || {}; - - if (isDescriptionUpdated) { - delayDebounceFn = setTimeout(() => { - editedDescriptionsLocal[mutationUrn] = updatedDescription; - localStorage.setItem(EDITED_DESCRIPTIONS_CACHE_NAME, JSON.stringify(editedDescriptionsLocal)); - }, 5000); - } - return () => clearTimeout(delayDebounceFn); - }, [mutationUrn, isDescriptionUpdated, updatedDescription, localStorageDictionary]); - // Handling the Discard Modal - const showModal = () => { - if (isDescriptionUpdated) { - setCancelModalVisible(true); + const handleConfirmClose = (showConfirm: boolean | undefined = true) => { + if (showConfirm && isDescriptionUpdated) { + setConfirmCloseModalVisible(true); } else if (onComplete) onComplete(); }; - function onCancel() { - setCancelModalVisible(false); - } - - const onDiscard = () => { + const handleCloseWithoutSaving = () => { delete editedDescriptions[mutationUrn]; if (Object.keys(editedDescriptions).length === 0) { localStorage.removeItem(EDITED_DESCRIPTIONS_CACHE_NAME); @@ -135,22 +133,19 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) = return entityData ? ( <> - - - - + - handleEditorChange(v)} /> + - {cancelModalVisible && ( + {confirmCloseModalVisible && ( setConfirmCloseModalVisible(false)} /> )} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditorToolbar.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditorToolbar.tsx new file mode 100644 index 0000000000000..6128a5f277c85 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionEditorToolbar.tsx @@ -0,0 +1,23 @@ +import { CheckOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import React from 'react'; +import TabToolbar from '../../../components/styled/TabToolbar'; + +type DescriptionEditorToolbarProps = { + disableSave: boolean; + onClose: () => void; + onSave: () => void; +}; + +export const DescriptionEditorToolbar = ({ disableSave, onClose, onSave }: DescriptionEditorToolbarProps) => { + return ( + + + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreview.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreview.tsx new file mode 100644 index 0000000000000..90f887976e1d3 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreview.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Editor } from './editor/Editor'; +import { DescriptionPreviewToolbar } from './DescriptionPreviewToolbar'; + +const EditorContainer = styled.div` + overflow: auto; + height: 100%; +`; + +type DescriptionPreviewProps = { + description: string; + onEdit: () => void; +}; + +export const DescriptionPreview = ({ description, onEdit }: DescriptionPreviewProps) => { + return ( + <> + + + + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewModal.tsx new file mode 100644 index 0000000000000..75045c50b43e7 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewModal.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { Modal } from 'antd'; +import ClickOutside from '../../../../../shared/ClickOutside'; +import { DescriptionEditor } from './DescriptionEditor'; +import { DescriptionPreview } from './DescriptionPreview'; +import { useRouteToTab } from '../../../EntityContext'; + +const modalStyle = { + top: '5%', + maxWidth: 1400, +}; + +const bodyStyle = { + margin: 0, + padding: 0, + height: '90vh', + display: 'flex', + flexDirection: 'column' as any, +}; + +type DescriptionPreviewModalProps = { + description: string; + editMode: boolean; + onClose: (showConfirm?: boolean) => void; +}; + +export const DescriptionPreviewModal = ({ description, editMode, onClose }: DescriptionPreviewModalProps) => { + const routeToTab = useRouteToTab(); + + const onConfirmClose = () => { + if (editMode) { + Modal.confirm({ + title: `Exit Editor`, + content: `Are you sure you want to exit the editor? Any unsaved changes will be lost.`, + onOk() { + onClose(); + }, + onCancel() {}, + okText: 'Yes', + maskClosable: true, + closable: true, + }); + } else { + onClose(false); + } + }; + + return ( + + + {(editMode && ( + routeToTab({ tabName: 'Documentation', tabParams: { modal: true } })} + /> + )) || ( + + routeToTab({ tabName: 'Documentation', tabParams: { editing: true, modal: true } }) + } + /> + )} + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewToolbar.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewToolbar.tsx new file mode 100644 index 0000000000000..590322190e780 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DescriptionPreviewToolbar.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { EditOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import TabToolbar from '../../../components/styled/TabToolbar'; + +type DescriptionPreviewToolbarProps = { + onEdit: () => void; +}; + +export const DescriptionPreviewToolbar = ({ onEdit }: DescriptionPreviewToolbarProps) => { + return ( + + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DiscardDescriptionModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DiscardDescriptionModal.tsx index 3ce46ba11f3c4..ce54fe711da83 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DiscardDescriptionModal.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DiscardDescriptionModal.tsx @@ -11,7 +11,7 @@ export const DiscardDescriptionModal = ({ cancelModalVisible, onDiscard, onCance return ( <> Cancel , - , + , ]} > -

Changes will not be saved. Do you want to proceed?

+

Are you sure you want to close the documentation editor? Any unsaved changes will be lost.

);