Skip to content

Commit

Permalink
feat(ui): Documentation Editor Improvements (#7072)
Browse files Browse the repository at this point in the history
  • Loading branch information
jjoyce0510 authored Jan 20, 2023
1 parent 65c2759 commit 72f0328
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 51 deletions.
6 changes: 6 additions & 0 deletions datahub-web-react/src/app/context/UserContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 (
<UserContext.Provider
value={{
Expand Down
51 changes: 51 additions & 0 deletions datahub-web-react/src/app/context/useInitialRedirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect } from 'react';
import { useHistory, useLocation } from 'react-router';
import { PageRoutes } from '../../conf/Global';

export function useInitialRedirect(state, localState, setState, setLocalState) {
const location = useLocation();
const history = useHistory();

/**
* Route to the most recently visited path once on first load of home page, if present in local storage.
*/
useEffect(() => {
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]);
}
7 changes: 7 additions & 0 deletions datahub-web-react/src/app/context/userContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { CorpUser, PlatformPrivileges } from '../../types.generated';
*/
export type LocalState = {
selectedViewUrn?: string | null;
selectedPath?: string | null;
selectedSearch?: string | null;
};

/**
Expand All @@ -19,6 +21,10 @@ export type State = {
loadedPersonalDefaultViewUrn: boolean;
hasSetDefaultView: boolean;
};
/**
* Whether the initial page path has been loaded.
*/
loadedInitialPath: boolean;
};

/**
Expand Down Expand Up @@ -47,6 +53,7 @@ export const DEFAULT_STATE: State = {
loadedPersonalDefaultViewUrn: false,
hasSetDefaultView: false,
},
loadedInitialPath: false,
};

export const DEFAULT_CONTEXT = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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 ? (
<>
<DescriptionEditor onComplete={() => routeToTab({ tabName: 'Documentation' })} />
</>
Expand All @@ -62,6 +67,19 @@ export const DocumentationTab = ({ properties }: { properties?: Props }) => {
</Button>
{!hideLinksButton && <AddLinkModal buttonProps={{ type: 'text' }} refetch={refetch} />}
</div>
<div>
<Button
type="text"
onClick={() =>
routeToTab({
tabName: 'Documentation',
tabParams: { modal: true },
})
}
>
<ExpandAltOutlined />
</Button>
</div>
</TabToolbar>
<div>
{description ? (
Expand All @@ -85,6 +103,15 @@ export const DocumentationTab = ({ properties }: { properties?: Props }) => {
{!hideLinksButton && <AddLinkModal refetch={refetch} />}
</EmptyTab>
)}
{showModal && (
<DescriptionPreviewModal
editMode={(isEditing && true) || false}
description={description}
onClose={() => {
routeToTab({ tabName: 'Documentation', tabParams: { editing: false } });
}}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
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`
overflow: auto;
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();
Expand All @@ -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<typeof setTimeout>;
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?.({
Expand All @@ -53,7 +69,7 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) =
});
};

const handleSaveDescription = async () => {
const handleSave = async () => {
message.loading({ content: 'Saving...' });
try {
if (updateEntity) {
Expand Down Expand Up @@ -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<typeof setTimeout>;
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);
Expand All @@ -135,22 +133,19 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) =

return entityData ? (
<>
<TabToolbar>
<Button type="text" onClick={showModal}>
Back
</Button>
<Button onClick={handleSaveDescription} disabled={!isDescriptionUpdated}>
<CheckOutlined /> Save
</Button>
</TabToolbar>
<DescriptionEditorToolbar
onSave={handleSave}
onClose={handleConfirmClose}
disableSave={!isDescriptionUpdated}
/>
<EditorContainer>
<Editor content={updatedDescription} onChange={(v) => handleEditorChange(v)} />
<Editor content={updatedDescription} onChange={handleEditorChange} />
</EditorContainer>
{cancelModalVisible && (
{confirmCloseModalVisible && (
<DiscardDescriptionModal
cancelModalVisible={cancelModalVisible}
onDiscard={onDiscard}
onCancel={onCancel}
cancelModalVisible={confirmCloseModalVisible}
onDiscard={handleCloseWithoutSaving}
onCancel={() => setConfirmCloseModalVisible(false)}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<TabToolbar>
<Button type="text" onClick={onClose}>
Back
</Button>
<Button onClick={onSave} disabled={disableSave}>
<CheckOutlined /> Save
</Button>
</TabToolbar>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<>
<DescriptionPreviewToolbar onEdit={onEdit} />
<EditorContainer>
<Editor content={description} readOnly />
</EditorContainer>
</>
);
};
Loading

0 comments on commit 72f0328

Please sign in to comment.