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 182c9a8e0c..737c00b159 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 @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import queryString from 'query-string'; import { useLocation } from 'react-router-dom'; @@ -10,26 +10,36 @@ import MDEditor from '@uiw/react-md-editor'; import TabToolbar from '../../components/styled/TabToolbar'; import { AddLinkModal } from '../../components/styled/AddLinkModal'; import { EmptyTab } from '../../components/styled/EmptyTab'; - import { DescriptionEditor } from './components/DescriptionEditor'; import { LinkList } from './components/LinkList'; + import { useEntityData, useRefetch, useRouteToTab } from '../../EntityContext'; +import { EDITED_DESCRIPTIONS_CACHE_NAME } from '../../utils'; const DocumentationContainer = styled.div` margin: 0 auto; padding: 40px 0; - max-width: 550px; + max-width: calc(100% - 10px); + margin: 0 32px; `; export const DocumentationTab = () => { - const { entityData } = useEntityData(); + const { urn, entityData } = useEntityData(); const refetch = useRefetch(); const description = entityData?.editableProperties?.description || entityData?.properties?.description || ''; const links = entityData?.institutionalMemory?.elements || []; + const localStorageDictionary = localStorage.getItem(EDITED_DESCRIPTIONS_CACHE_NAME); const routeToTab = useRouteToTab(); const isEditing = queryString.parse(useLocation().search, { parseBooleans: true }).editing; + useEffect(() => { + const editedDescriptions = (localStorageDictionary && JSON.parse(localStorageDictionary)) || {}; + if (editedDescriptions.hasOwnProperty(urn)) { + routeToTab({ tabName: 'Documentation', tabParams: { editing: true } }); + } + }, [urn, routeToTab, localStorageDictionary]); + return isEditing ? ( <> routeToTab({ tabName: 'Documentation' })} /> 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 64e4a83171..1960698569 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,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { message, Button } from 'antd'; import { CheckOutlined } from '@ant-design/icons'; @@ -10,6 +10,8 @@ import TabToolbar from '../../../components/styled/TabToolbar'; import { GenericEntityUpdate } from '../../../types'; import { useEntityData, useEntityUpdate, useRefetch } from '../../../EntityContext'; import { useUpdateDescriptionMutation } from '../../../../../../graphql/mutations.generated'; +import { DiscardDescriptionModal } from './DiscardDescriptionModal'; +import { EDITED_DESCRIPTIONS_CACHE_NAME } from '../../../utils'; export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) => { const { urn, entityType, entityData } = useEntityData(); @@ -17,8 +19,15 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) = const updateEntity = useEntityUpdate(); const [updateDescriptionMutation] = useUpdateDescriptionMutation(); - const description = entityData?.editableProperties?.description || entityData?.properties?.description || ''; + const localStorageDictionary = localStorage.getItem(EDITED_DESCRIPTIONS_CACHE_NAME); + const editedDescriptions = (localStorageDictionary && JSON.parse(localStorageDictionary)) || {}; + const description = editedDescriptions.hasOwnProperty(urn) + ? editedDescriptions[urn] + : entityData?.editableProperties?.description || entityData?.properties?.description || ''; + const [updatedDescription, setUpdatedDescription] = useState(description); + const [isDescriptionUpdated, setIsDescriptionUpdated] = useState(editedDescriptions.hasOwnProperty(urn)); + const [cancelModalVisible, setCancelModalVisible] = useState(false); const updateDescriptionLegacy = () => { return updateEntity?.({ @@ -55,6 +64,13 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) = entityUrn: urn, }); message.success({ content: 'Description Updated', duration: 2 }); + // Updating the localStorage after save + delete editedDescriptions[urn]; + if (Object.keys(editedDescriptions).length === 0) { + localStorage.removeItem(EDITED_DESCRIPTIONS_CACHE_NAME); + } else { + localStorage.setItem(EDITED_DESCRIPTIONS_CACHE_NAME, JSON.stringify(editedDescriptions)); + } if (onComplete) onComplete(); } catch (e: unknown) { message.destroy(); @@ -65,22 +81,74 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) = refetch?.(); }; + // Function to handle all changes in Editor + const handleEditorChange = (editedDescription: string) => { + setUpdatedDescription(editedDescription); + if (editedDescription === description) { + setIsDescriptionUpdated(false); + } else { + setIsDescriptionUpdated(true); + } + }; + + // 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[urn] = updatedDescription; + localStorage.setItem(EDITED_DESCRIPTIONS_CACHE_NAME, JSON.stringify(editedDescriptionsLocal)); + }, 5000); + } + return () => clearTimeout(delayDebounceFn); + }, [urn, isDescriptionUpdated, updatedDescription, localStorageDictionary]); + + // Handling the Discard Modal + const showModal = () => { + if (isDescriptionUpdated) { + setCancelModalVisible(true); + } else if (onComplete) onComplete(); + }; + + function onCancel() { + setCancelModalVisible(false); + } + + const onDiscard = () => { + delete editedDescriptions[urn]; + if (Object.keys(editedDescriptions).length === 0) { + localStorage.removeItem(EDITED_DESCRIPTIONS_CACHE_NAME); + } else { + localStorage.setItem(EDITED_DESCRIPTIONS_CACHE_NAME, JSON.stringify(editedDescriptions)); + } + if (onComplete) onComplete(); + }; + return entityData ? ( <> - - setUpdatedDescription(v || '')} + onChange={(v) => handleEditorChange(v || '')} preview="live" visiableDragbar={false} /> + {cancelModalVisible && ( + + )} ) : null; }; 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 new file mode 100644 index 0000000000..3ce46ba11f --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/DiscardDescriptionModal.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Modal, Button } from 'antd'; + +type Props = { + cancelModalVisible?: boolean; + onDiscard?: () => void; + onCancel?: () => void; +}; + +export const DiscardDescriptionModal = ({ cancelModalVisible, onDiscard, onCancel }: Props) => { + return ( + <> + + Cancel + , + , + ]} + > +

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

+
+ + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/utils.ts b/datahub-web-react/src/app/entity/shared/utils.ts index 18806c3f94..dab5119959 100644 --- a/datahub-web-react/src/app/entity/shared/utils.ts +++ b/datahub-web-react/src/app/entity/shared/utils.ts @@ -57,3 +57,5 @@ export const singularizeCollectionName = (collectionName: string): string => { return collectionName; }; + +export const EDITED_DESCRIPTIONS_CACHE_NAME = 'editedDescriptions'; diff --git a/datahub-web-react/src/app/shared/tags/AddTagTermModal.tsx b/datahub-web-react/src/app/shared/tags/AddTagTermModal.tsx index 472d57d9bd..3c300f45cd 100644 --- a/datahub-web-react/src/app/shared/tags/AddTagTermModal.tsx +++ b/datahub-web-react/src/app/shared/tags/AddTagTermModal.tsx @@ -2,14 +2,8 @@ import React, { useState } from 'react'; import { message, Button, Modal, Select, Typography } from 'antd'; import styled from 'styled-components'; -import { useGetAutoCompleteMultipleResultsLazyQuery } from '../../../graphql/search.generated'; -import { - GlobalTags, - EntityType, - AutoCompleteResultForEntity, - GlossaryTerms, - SubResourceType, -} from '../../../types.generated'; +import { useGetSearchResultsLazyQuery } from '../../../graphql/search.generated'; +import { GlobalTags, EntityType, GlossaryTerms, SubResourceType, SearchResult } from '../../../types.generated'; import CreateTagModal from './CreateTagModal'; import { useEntityRegistry } from '../../useEntityRegistry'; import { IconStyleType } from '../../entity/Entity'; @@ -74,9 +68,6 @@ export default function AddTagTermModal({ entitySubresource, type = EntityType.Tag, }: AddTagModalProps) { - const [getAutoCompleteResults, { loading, data: suggestionsData }] = useGetAutoCompleteMultipleResultsLazyQuery({ - fetchPolicy: 'no-cache', - }); const [inputValue, setInputValue] = useState(''); const [selectedValue, setSelectedValue] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); @@ -84,39 +75,49 @@ export default function AddTagTermModal({ const entityRegistry = useEntityRegistry(); const [addTagMutation] = useAddTagMutation(); const [addTermMutation] = useAddTermMutation(); + const [tagSearch, { data: tagSearchData }] = useGetSearchResultsLazyQuery(); + const tagSearchResults = tagSearchData?.search?.searchResults || []; - const autoComplete = (query: string) => { - if (query && query !== '') { - getAutoCompleteResults({ + const handleSearch = (text: string) => { + if (text.length > 0) { + tagSearch({ variables: { input: { - types: [type], - query, - limit: 25, + type: EntityType.Tag, + query: text, + start: 0, + count: 10, }, }, }); } }; - const options = - suggestionsData?.autoCompleteForMultiple?.suggestions.flatMap((entity: AutoCompleteResultForEntity) => - entity.suggestions.map((suggestion: string) => - renderItem(suggestion, entityRegistry.getIcon(entity.type, 14, IconStyleType.TAB_VIEW), entity.type), - ), - ) || []; - - const inputExistsInAutocomplete = options.some((option) => option.value.toLowerCase() === inputValue.toLowerCase()); - - const autocompleteOptions = - options.map((option) => ( - - {option.label} + const renderSearchResult = (result: SearchResult) => { + const displayName = entityRegistry.getDisplayName(result.entity.type, result.entity); + const item = renderItem( + displayName, + entityRegistry.getIcon(result.entity.type, 14, IconStyleType.ACCENT), + result.entity.type, + ); + return ( + + {item.label} - )) || []; + ); + }; - if (!inputExistsInAutocomplete && inputValue.length > 0 && !loading && type === EntityType.Tag) { - autocompleteOptions.push( + const tagSearchOptions = tagSearchResults.map((result) => { + return renderSearchResult(result); + }); + + const inputExistsInTagSearch = tagSearchResults.some((result: SearchResult) => { + const displayName = entityRegistry.getDisplayName(result.entity.type, result.entity); + return displayName.toLowerCase() === inputValue.toLowerCase(); + }); + + if (!inputExistsInTagSearch && inputValue.length > 0 && type === EntityType.Tag) { + tagSearchOptions.push( Create {inputValue} , @@ -250,20 +251,19 @@ export default function AddTagTermModal({ allowClear autoFocus showSearch - placeholder={`Find a ${entityRegistry.getEntityName(type)?.toLowerCase()}`} + placeholder={`Search for ${entityRegistry.getEntityName(type)?.toLowerCase()}...`} defaultActiveFirstOption={false} showArrow={false} filterOption={false} onSearch={(value: string) => { - autoComplete(value.trim()); + handleSearch(value.trim()); setInputValue(value.trim()); }} onSelect={(selected) => selected === CREATE_TAG_VALUE ? setShowCreateModal(true) : setSelectedValue(String(selected)) } - notFoundContent={loading ? 'loading' : 'type to search'} > - {autocompleteOptions} + {tagSearchOptions} ); diff --git a/datahub-web-react/src/app/shared/tags/CreateTagModal.tsx b/datahub-web-react/src/app/shared/tags/CreateTagModal.tsx index c3f4ae6d54..db2ab8301b 100644 --- a/datahub-web-react/src/app/shared/tags/CreateTagModal.tsx +++ b/datahub-web-react/src/app/shared/tags/CreateTagModal.tsx @@ -75,6 +75,7 @@ export default function CreateTagModal({