mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-03 14:16:28 +00:00
Fixed auto complete pr coments (#4072)
This commit is contained in:
parent
8002fd8eb6
commit
a0a2e02c5c
@ -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 ? (
|
||||
<>
|
||||
<DescriptionEditor onComplete={() => routeToTab({ tabName: 'Documentation' })} />
|
||||
|
||||
@ -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<GenericEntityUpdate>();
|
||||
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<typeof setTimeout>;
|
||||
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 ? (
|
||||
<>
|
||||
<TabToolbar>
|
||||
<Button type="text" onClick={onComplete}>
|
||||
Cancel
|
||||
<Button type="text" onClick={showModal}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={handleSaveDescription}>
|
||||
<Button onClick={handleSaveDescription} disabled={!isDescriptionUpdated}>
|
||||
<CheckOutlined /> Save
|
||||
</Button>
|
||||
</TabToolbar>
|
||||
<StyledMDEditor
|
||||
value={description}
|
||||
onChange={(v) => setUpdatedDescription(v || '')}
|
||||
onChange={(v) => handleEditorChange(v || '')}
|
||||
preview="live"
|
||||
visiableDragbar={false}
|
||||
/>
|
||||
{cancelModalVisible && (
|
||||
<DiscardDescriptionModal
|
||||
cancelModalVisible={cancelModalVisible}
|
||||
onDiscard={onDiscard}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<Modal
|
||||
title="Discard Changes"
|
||||
visible={cancelModalVisible}
|
||||
destroyOnClose
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button type="text" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button onClick={onDiscard}>Discard</Button>,
|
||||
]}
|
||||
>
|
||||
<p>Changes will not be saved. Do you want to proceed?</p>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -57,3 +57,5 @@ export const singularizeCollectionName = (collectionName: string): string => {
|
||||
|
||||
return collectionName;
|
||||
};
|
||||
|
||||
export const EDITED_DESCRIPTIONS_CACHE_NAME = 'editedDescriptions';
|
||||
|
||||
@ -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) => (
|
||||
<Select.Option value={`${option.value}${NAME_TYPE_SEPARATOR}${option.type}`} key={option.value}>
|
||||
{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 (
|
||||
<Select.Option value={`${item.value}${NAME_TYPE_SEPARATOR}${item.type}`} key={item.value}>
|
||||
{item.label}
|
||||
</Select.Option>
|
||||
)) || [];
|
||||
);
|
||||
};
|
||||
|
||||
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(
|
||||
<Select.Option value={CREATE_TAG_VALUE} key={CREATE_TAG_VALUE}>
|
||||
<Typography.Link> Create {inputValue}</Typography.Link>
|
||||
</Select.Option>,
|
||||
@ -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}
|
||||
</TagSelect>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -75,6 +75,7 @@ export default function CreateTagModal({
|
||||
<Modal
|
||||
title={`Create ${tagName}`}
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onBack} type="text">
|
||||
|
||||
@ -111,10 +111,10 @@ describe('TagTermGroup', () => {
|
||||
</MockedProvider>,
|
||||
);
|
||||
expect(queryByText('Add Tag')).toBeInTheDocument();
|
||||
expect(queryByText('Find a tag')).not.toBeInTheDocument();
|
||||
expect(queryByText('Search for tag...')).not.toBeInTheDocument();
|
||||
const AddTagButton = getByText('Add Tag');
|
||||
fireEvent.click(AddTagButton);
|
||||
expect(queryByText('Find a tag')).toBeInTheDocument();
|
||||
expect(queryByText('Search for tag...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders create term', () => {
|
||||
@ -133,10 +133,10 @@ describe('TagTermGroup', () => {
|
||||
</MockedProvider>,
|
||||
);
|
||||
expect(queryByText('Add Term')).toBeInTheDocument();
|
||||
expect(queryByText('Find a glossary term')).not.toBeInTheDocument();
|
||||
expect(queryByText('Search for glossary term...')).not.toBeInTheDocument();
|
||||
const AddTagButton = getByText('Add Term');
|
||||
fireEvent.click(AddTagButton);
|
||||
expect(queryByText('Find a glossary term')).toBeInTheDocument();
|
||||
expect(queryByText('Search for glossary term...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders terms', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user