mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-05 15:13:21 +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 queryString from 'query-string';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
@ -10,26 +10,36 @@ import MDEditor from '@uiw/react-md-editor';
|
|||||||
import TabToolbar from '../../components/styled/TabToolbar';
|
import TabToolbar from '../../components/styled/TabToolbar';
|
||||||
import { AddLinkModal } from '../../components/styled/AddLinkModal';
|
import { AddLinkModal } from '../../components/styled/AddLinkModal';
|
||||||
import { EmptyTab } from '../../components/styled/EmptyTab';
|
import { EmptyTab } from '../../components/styled/EmptyTab';
|
||||||
|
|
||||||
import { DescriptionEditor } from './components/DescriptionEditor';
|
import { DescriptionEditor } from './components/DescriptionEditor';
|
||||||
import { LinkList } from './components/LinkList';
|
import { LinkList } from './components/LinkList';
|
||||||
|
|
||||||
import { useEntityData, useRefetch, useRouteToTab } from '../../EntityContext';
|
import { useEntityData, useRefetch, useRouteToTab } from '../../EntityContext';
|
||||||
|
import { EDITED_DESCRIPTIONS_CACHE_NAME } from '../../utils';
|
||||||
|
|
||||||
const DocumentationContainer = styled.div`
|
const DocumentationContainer = styled.div`
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
max-width: 550px;
|
max-width: calc(100% - 10px);
|
||||||
|
margin: 0 32px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DocumentationTab = () => {
|
export const DocumentationTab = () => {
|
||||||
const { entityData } = useEntityData();
|
const { urn, entityData } = useEntityData();
|
||||||
const refetch = useRefetch();
|
const refetch = useRefetch();
|
||||||
const description = entityData?.editableProperties?.description || entityData?.properties?.description || '';
|
const description = entityData?.editableProperties?.description || entityData?.properties?.description || '';
|
||||||
const links = entityData?.institutionalMemory?.elements || [];
|
const links = entityData?.institutionalMemory?.elements || [];
|
||||||
|
const localStorageDictionary = localStorage.getItem(EDITED_DESCRIPTIONS_CACHE_NAME);
|
||||||
|
|
||||||
const routeToTab = useRouteToTab();
|
const routeToTab = useRouteToTab();
|
||||||
const isEditing = queryString.parse(useLocation().search, { parseBooleans: true }).editing;
|
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 ? (
|
return isEditing ? (
|
||||||
<>
|
<>
|
||||||
<DescriptionEditor onComplete={() => routeToTab({ tabName: 'Documentation' })} />
|
<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 { message, Button } from 'antd';
|
||||||
import { CheckOutlined } from '@ant-design/icons';
|
import { CheckOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
@ -10,6 +10,8 @@ import TabToolbar from '../../../components/styled/TabToolbar';
|
|||||||
import { GenericEntityUpdate } from '../../../types';
|
import { GenericEntityUpdate } from '../../../types';
|
||||||
import { useEntityData, useEntityUpdate, useRefetch } from '../../../EntityContext';
|
import { useEntityData, useEntityUpdate, useRefetch } from '../../../EntityContext';
|
||||||
import { useUpdateDescriptionMutation } from '../../../../../../graphql/mutations.generated';
|
import { useUpdateDescriptionMutation } from '../../../../../../graphql/mutations.generated';
|
||||||
|
import { DiscardDescriptionModal } from './DiscardDescriptionModal';
|
||||||
|
import { EDITED_DESCRIPTIONS_CACHE_NAME } from '../../../utils';
|
||||||
|
|
||||||
export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) => {
|
export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) => {
|
||||||
const { urn, entityType, entityData } = useEntityData();
|
const { urn, entityType, entityData } = useEntityData();
|
||||||
@ -17,8 +19,15 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) =
|
|||||||
const updateEntity = useEntityUpdate<GenericEntityUpdate>();
|
const updateEntity = useEntityUpdate<GenericEntityUpdate>();
|
||||||
const [updateDescriptionMutation] = useUpdateDescriptionMutation();
|
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 [updatedDescription, setUpdatedDescription] = useState(description);
|
||||||
|
const [isDescriptionUpdated, setIsDescriptionUpdated] = useState(editedDescriptions.hasOwnProperty(urn));
|
||||||
|
const [cancelModalVisible, setCancelModalVisible] = useState(false);
|
||||||
|
|
||||||
const updateDescriptionLegacy = () => {
|
const updateDescriptionLegacy = () => {
|
||||||
return updateEntity?.({
|
return updateEntity?.({
|
||||||
@ -55,6 +64,13 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) =
|
|||||||
entityUrn: urn,
|
entityUrn: urn,
|
||||||
});
|
});
|
||||||
message.success({ content: 'Description Updated', duration: 2 });
|
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();
|
if (onComplete) onComplete();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.destroy();
|
message.destroy();
|
||||||
@ -65,22 +81,74 @@ export const DescriptionEditor = ({ onComplete }: { onComplete?: () => void }) =
|
|||||||
refetch?.();
|
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 ? (
|
return entityData ? (
|
||||||
<>
|
<>
|
||||||
<TabToolbar>
|
<TabToolbar>
|
||||||
<Button type="text" onClick={onComplete}>
|
<Button type="text" onClick={showModal}>
|
||||||
Cancel
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSaveDescription}>
|
<Button onClick={handleSaveDescription} disabled={!isDescriptionUpdated}>
|
||||||
<CheckOutlined /> Save
|
<CheckOutlined /> Save
|
||||||
</Button>
|
</Button>
|
||||||
</TabToolbar>
|
</TabToolbar>
|
||||||
<StyledMDEditor
|
<StyledMDEditor
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(v) => setUpdatedDescription(v || '')}
|
onChange={(v) => handleEditorChange(v || '')}
|
||||||
preview="live"
|
preview="live"
|
||||||
visiableDragbar={false}
|
visiableDragbar={false}
|
||||||
/>
|
/>
|
||||||
|
{cancelModalVisible && (
|
||||||
|
<DiscardDescriptionModal
|
||||||
|
cancelModalVisible={cancelModalVisible}
|
||||||
|
onDiscard={onDiscard}
|
||||||
|
onCancel={onCancel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : null;
|
) : 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;
|
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 { message, Button, Modal, Select, Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { useGetAutoCompleteMultipleResultsLazyQuery } from '../../../graphql/search.generated';
|
import { useGetSearchResultsLazyQuery } from '../../../graphql/search.generated';
|
||||||
import {
|
import { GlobalTags, EntityType, GlossaryTerms, SubResourceType, SearchResult } from '../../../types.generated';
|
||||||
GlobalTags,
|
|
||||||
EntityType,
|
|
||||||
AutoCompleteResultForEntity,
|
|
||||||
GlossaryTerms,
|
|
||||||
SubResourceType,
|
|
||||||
} from '../../../types.generated';
|
|
||||||
import CreateTagModal from './CreateTagModal';
|
import CreateTagModal from './CreateTagModal';
|
||||||
import { useEntityRegistry } from '../../useEntityRegistry';
|
import { useEntityRegistry } from '../../useEntityRegistry';
|
||||||
import { IconStyleType } from '../../entity/Entity';
|
import { IconStyleType } from '../../entity/Entity';
|
||||||
@ -74,9 +68,6 @@ export default function AddTagTermModal({
|
|||||||
entitySubresource,
|
entitySubresource,
|
||||||
type = EntityType.Tag,
|
type = EntityType.Tag,
|
||||||
}: AddTagModalProps) {
|
}: AddTagModalProps) {
|
||||||
const [getAutoCompleteResults, { loading, data: suggestionsData }] = useGetAutoCompleteMultipleResultsLazyQuery({
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
});
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [selectedValue, setSelectedValue] = useState('');
|
const [selectedValue, setSelectedValue] = useState('');
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
@ -84,39 +75,49 @@ export default function AddTagTermModal({
|
|||||||
const entityRegistry = useEntityRegistry();
|
const entityRegistry = useEntityRegistry();
|
||||||
const [addTagMutation] = useAddTagMutation();
|
const [addTagMutation] = useAddTagMutation();
|
||||||
const [addTermMutation] = useAddTermMutation();
|
const [addTermMutation] = useAddTermMutation();
|
||||||
|
const [tagSearch, { data: tagSearchData }] = useGetSearchResultsLazyQuery();
|
||||||
|
const tagSearchResults = tagSearchData?.search?.searchResults || [];
|
||||||
|
|
||||||
const autoComplete = (query: string) => {
|
const handleSearch = (text: string) => {
|
||||||
if (query && query !== '') {
|
if (text.length > 0) {
|
||||||
getAutoCompleteResults({
|
tagSearch({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
types: [type],
|
type: EntityType.Tag,
|
||||||
query,
|
query: text,
|
||||||
limit: 25,
|
start: 0,
|
||||||
|
count: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const options =
|
const renderSearchResult = (result: SearchResult) => {
|
||||||
suggestionsData?.autoCompleteForMultiple?.suggestions.flatMap((entity: AutoCompleteResultForEntity) =>
|
const displayName = entityRegistry.getDisplayName(result.entity.type, result.entity);
|
||||||
entity.suggestions.map((suggestion: string) =>
|
const item = renderItem(
|
||||||
renderItem(suggestion, entityRegistry.getIcon(entity.type, 14, IconStyleType.TAB_VIEW), entity.type),
|
displayName,
|
||||||
),
|
entityRegistry.getIcon(result.entity.type, 14, IconStyleType.ACCENT),
|
||||||
) || [];
|
result.entity.type,
|
||||||
|
);
|
||||||
const inputExistsInAutocomplete = options.some((option) => option.value.toLowerCase() === inputValue.toLowerCase());
|
return (
|
||||||
|
<Select.Option value={`${item.value}${NAME_TYPE_SEPARATOR}${item.type}`} key={item.value}>
|
||||||
const autocompleteOptions =
|
{item.label}
|
||||||
options.map((option) => (
|
|
||||||
<Select.Option value={`${option.value}${NAME_TYPE_SEPARATOR}${option.type}`} key={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
)) || [];
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (!inputExistsInAutocomplete && inputValue.length > 0 && !loading && type === EntityType.Tag) {
|
const tagSearchOptions = tagSearchResults.map((result) => {
|
||||||
autocompleteOptions.push(
|
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}>
|
<Select.Option value={CREATE_TAG_VALUE} key={CREATE_TAG_VALUE}>
|
||||||
<Typography.Link> Create {inputValue}</Typography.Link>
|
<Typography.Link> Create {inputValue}</Typography.Link>
|
||||||
</Select.Option>,
|
</Select.Option>,
|
||||||
@ -250,20 +251,19 @@ export default function AddTagTermModal({
|
|||||||
allowClear
|
allowClear
|
||||||
autoFocus
|
autoFocus
|
||||||
showSearch
|
showSearch
|
||||||
placeholder={`Find a ${entityRegistry.getEntityName(type)?.toLowerCase()}`}
|
placeholder={`Search for ${entityRegistry.getEntityName(type)?.toLowerCase()}...`}
|
||||||
defaultActiveFirstOption={false}
|
defaultActiveFirstOption={false}
|
||||||
showArrow={false}
|
showArrow={false}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={(value: string) => {
|
onSearch={(value: string) => {
|
||||||
autoComplete(value.trim());
|
handleSearch(value.trim());
|
||||||
setInputValue(value.trim());
|
setInputValue(value.trim());
|
||||||
}}
|
}}
|
||||||
onSelect={(selected) =>
|
onSelect={(selected) =>
|
||||||
selected === CREATE_TAG_VALUE ? setShowCreateModal(true) : setSelectedValue(String(selected))
|
selected === CREATE_TAG_VALUE ? setShowCreateModal(true) : setSelectedValue(String(selected))
|
||||||
}
|
}
|
||||||
notFoundContent={loading ? 'loading' : 'type to search'}
|
|
||||||
>
|
>
|
||||||
{autocompleteOptions}
|
{tagSearchOptions}
|
||||||
</TagSelect>
|
</TagSelect>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export default function CreateTagModal({
|
|||||||
<Modal
|
<Modal
|
||||||
title={`Create ${tagName}`}
|
title={`Create ${tagName}`}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
|
onCancel={onClose}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button onClick={onBack} type="text">
|
<Button onClick={onBack} type="text">
|
||||||
|
|||||||
@ -111,10 +111,10 @@ describe('TagTermGroup', () => {
|
|||||||
</MockedProvider>,
|
</MockedProvider>,
|
||||||
);
|
);
|
||||||
expect(queryByText('Add Tag')).toBeInTheDocument();
|
expect(queryByText('Add Tag')).toBeInTheDocument();
|
||||||
expect(queryByText('Find a tag')).not.toBeInTheDocument();
|
expect(queryByText('Search for tag...')).not.toBeInTheDocument();
|
||||||
const AddTagButton = getByText('Add Tag');
|
const AddTagButton = getByText('Add Tag');
|
||||||
fireEvent.click(AddTagButton);
|
fireEvent.click(AddTagButton);
|
||||||
expect(queryByText('Find a tag')).toBeInTheDocument();
|
expect(queryByText('Search for tag...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders create term', () => {
|
it('renders create term', () => {
|
||||||
@ -133,10 +133,10 @@ describe('TagTermGroup', () => {
|
|||||||
</MockedProvider>,
|
</MockedProvider>,
|
||||||
);
|
);
|
||||||
expect(queryByText('Add Term')).toBeInTheDocument();
|
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');
|
const AddTagButton = getByText('Add Term');
|
||||||
fireEvent.click(AddTagButton);
|
fireEvent.click(AddTagButton);
|
||||||
expect(queryByText('Find a glossary term')).toBeInTheDocument();
|
expect(queryByText('Search for glossary term...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders terms', () => {
|
it('renders terms', () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user