From e4572e97bf7d502c1f9d41c6eaee38c2945484b6 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 31 Aug 2022 20:43:55 +0200 Subject: [PATCH] RelationInputWrapper: Fix prop-types, add mock data --- .../components/RelationInput/RelationInput.js | 221 ++++++++++++++++++ .../components/RelationInput/index.js | 216 +---------------- .../RelationInputWrapper.js | 45 +++- .../utils/getRelationLink.js | 2 +- .../utils/normalizeRelations.js | 19 +- .../RelationInputWrapper/utils/select.js | 2 +- .../utils/tests/getRelationLink.test.js | 6 +- .../hooks/useRelation/useRelation.js | 139 +++++++++-- 8 files changed, 395 insertions(+), 255 deletions(-) create mode 100644 packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js b/packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js new file mode 100644 index 0000000000..4d8d1ad7f9 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/RelationInput/RelationInput.js @@ -0,0 +1,221 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styled from 'styled-components'; + +import { ReactSelect } from '@strapi/helper-plugin'; +import { Badge } from '@strapi/design-system/Badge'; +import { Box } from '@strapi/design-system/Box'; +import { BaseLink } from '@strapi/design-system/BaseLink'; +import { Icon } from '@strapi/design-system/Icon'; +import { FieldLabel, FieldError, FieldHint, Field } from '@strapi/design-system/Field'; +import { TextButton } from '@strapi/design-system/TextButton'; +import { Typography } from '@strapi/design-system/Typography'; +import { Loader } from '@strapi/design-system/Loader'; + +import Cross from '@strapi/icons/Cross'; +import Refresh from '@strapi/icons/Refresh'; + +import { Relation } from './components/Relation'; +import { RelationItem } from './components/RelationItem'; +import { RelationList } from './components/RelationList'; +import { Option } from './components/Option'; + +const RelationItemCenterChildren = styled(RelationItem)` + div { + justify-content: center; + } +`; + +const RelationInput = ({ + description, + disabled, + error, + id, + name, + label, + labelLoadMore, + listHeight, + loadingMessage, + relations, + onRelationAdd, + onRelationLoadMore, + onSearchClose, + onSearchOpen, + onRelationRemove, + onSearchNextPage, + onSearch, + placeholder, + publicationStateTranslations, + searchResults, +}) => { + return ( + + + {label} + ({ + ...result, + value: result.id, + label: result.mainField, + }))} + isDisabled={disabled} + isLoading={searchResults.isLoading} + error={error} + inputId={id} + isSearchable + isClear + loadingMessage={() => loadingMessage} + onChange={onRelationAdd} + onInputChange={onSearch} + onMenuClose={onSearchClose} + onMenuOpen={onSearchOpen} + onMenuScrollToBottom={onSearchNextPage} + placeholder={placeholder} + /> + + } + loadMore={ + !disabled && + labelLoadMore && ( + onRelationLoadMore()} startIcon={}> + {labelLoadMore} + + ) + } + > + + {relations.isSuccess && + relations.data.pages.flat().map((relation) => { + const { publicationState, href, mainField, id } = relation; + const badgeColor = publicationState === 'draft' ? 'secondary' : 'success'; + + return ( + onRelationRemove(relation)} + > + + + } + > + + {href ? ( + + + {mainField} + + + ) : ( + + {mainField} + + )} + + + {publicationState && ( + + {publicationStateTranslations[publicationState]} + + )} + + ); + })} + {relations.isLoading && ( + + {loadingMessage} + + )} + + + + + + + + ); +}; + +const ReactQueryRelationResult = PropTypes.shape({ + data: PropTypes.shape({ + pages: PropTypes.arrayOf( + PropTypes.arrayOf( + PropTypes.shape({ + href: PropTypes.string, + id: PropTypes.number.isRequired, + publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }) + ) + ), + }), + isLoading: PropTypes.bool.isRequired, + isSuccess: PropTypes.bool.isRequired, +}); + +const ReactQuerySearchResult = PropTypes.shape({ + data: PropTypes.shape({ + pages: PropTypes.arrayOf( + PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number.isRequired, + mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + }) + ) + ), + }), + isLoading: PropTypes.bool.isRequired, + isSuccess: PropTypes.bool.isRequired, +}); + +RelationInput.defaultProps = { + description: undefined, + disabled: false, + error: undefined, + labelLoadMore: null, + listHeight: undefined, + relations: [], + searchResults: [], +}; + +RelationInput.propTypes = { + error: PropTypes.string, + description: PropTypes.string, + disabled: PropTypes.bool, + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + labelLoadMore: PropTypes.string, + listHeight: PropTypes.string, + loadingMessage: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + onRelationAdd: PropTypes.func.isRequired, + onRelationRemove: PropTypes.func.isRequired, + onRelationLoadMore: PropTypes.func.isRequired, + onSearch: PropTypes.func.isRequired, + onSearchNextPage: PropTypes.func.isRequired, + onSearchClose: PropTypes.func.isRequired, + onSearchOpen: PropTypes.func.isRequired, + placeholder: PropTypes.string.isRequired, + publicationStateTranslations: PropTypes.shape({ + draft: PropTypes.string.isRequired, + published: PropTypes.string.isRequired, + }).isRequired, + searchResults: ReactQuerySearchResult, + relations: ReactQueryRelationResult, +}; + +export default RelationInput; diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInput/index.js b/packages/core/admin/admin/src/content-manager/components/RelationInput/index.js index 60b200aebf..c3f06f6c7e 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInput/index.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInput/index.js @@ -1,215 +1 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import styled from 'styled-components'; - -import { ReactSelect } from '@strapi/helper-plugin'; -import { Badge } from '@strapi/design-system/Badge'; -import { Box } from '@strapi/design-system/Box'; -import { BaseLink } from '@strapi/design-system/BaseLink'; -import { Icon } from '@strapi/design-system/Icon'; -import { FieldLabel, FieldError, FieldHint, Field } from '@strapi/design-system/Field'; -import { TextButton } from '@strapi/design-system/TextButton'; -import { Typography } from '@strapi/design-system/Typography'; -import { Loader } from '@strapi/design-system/Loader'; - -import Cross from '@strapi/icons/Cross'; -import Refresh from '@strapi/icons/Refresh'; - -import { Relation } from './components/Relation'; -import { RelationItem } from './components/RelationItem'; -import { RelationList } from './components/RelationList'; -import { Option } from './components/Option'; - -const RelationItemCenterChildren = styled(RelationItem)` - div { - justify-content: center; - } -`; - -const RelationInput = ({ - description, - disabled, - error, - id, - name, - label, - labelLoadMore, - listHeight, - loadingMessage, - relations, - onRelationClose, - onRelationAdd, - onRelationLoadMore, - onRelationOpen, - onRelationRemove, - onSearchNextPage, - onSearch, - placeholder, - publicationStateTranslations, - searchResults, -}) => { - return ( - - - {label} - loadingMessage} - onChange={onRelationAdd} - onInputChange={onSearch} - onMenuClose={onRelationOpen} - onMenuOpen={onRelationClose} - onMenuScrollToBottom={onSearchNextPage} - placeholder={placeholder} - /> - - } - loadMore={ - !disabled && ( - onRelationLoadMore()} startIcon={}> - {labelLoadMore} - - ) - } - > - - {relations.isSuccess && - relations.data.pages.flat().map((relation) => { - const { publicationState, href, mainField, id } = relation; - const badgeColor = publicationState === 'draft' ? 'secondary' : 'success'; - - return ( - onRelationRemove(relation)} - > - - - } - > - - {href ? ( - - - {mainField} - - - ) : ( - - {mainField} - - )} - - - {publicationState && ( - - {publicationStateTranslations[publicationState]} - - )} - - ); - })} - {relations.isLoading && ( - - {loadingMessage} - - )} - - - - - - - - ); -}; - -const ReactQueryRelationResult = PropTypes.shape({ - data: PropTypes.shape({ - pages: PropTypes.arrayOf( - PropTypes.arrayOf( - PropTypes.shape({ - href: PropTypes.string, - id: PropTypes.number.isRequired, - publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }) - ) - ), - }), - isLoading: PropTypes.bool.isRequired, - isSuccess: PropTypes.bool.isRequired, -}); - -const ReactQuerySearchResult = PropTypes.shape({ - data: PropTypes.shape({ - pages: PropTypes.arrayOf( - PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.number.isRequired, - mainField: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - publicationState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - }) - ) - ), - }), - isLoading: PropTypes.bool.isRequired, - isSuccess: PropTypes.bool.isRequired, -}); - -RelationInput.defaultProps = { - description: undefined, - disabled: false, - error: undefined, - listHeight: undefined, - relations: [], - searchResults: [], -}; - -RelationInput.propTypes = { - error: PropTypes.string, - description: PropTypes.string, - disabled: PropTypes.bool, - id: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - labelLoadMore: PropTypes.string.isRequired, - listHeight: PropTypes.string, - loadingMessage: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - onRelationAdd: PropTypes.func.isRequired, - onRelationOpen: PropTypes.func.isRequired, - onRelationClose: PropTypes.func.isRequired, - onRelationRemove: PropTypes.func.isRequired, - onRelationLoadMore: PropTypes.func.isRequired, - onSearch: PropTypes.func.isRequired, - onSearchNextPage: PropTypes.func.isRequired, - placeholder: PropTypes.string.isRequired, - publicationStateTranslations: PropTypes.shape({ - draft: PropTypes.string.isRequired, - published: PropTypes.string.isRequired, - }).isRequired, - searchResults: ReactQuerySearchResult, - relations: ReactQueryRelationResult, -}; - -export default RelationInput; +export { default as RelationInput } from './RelationInput'; diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/RelationInputWrapper.js b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/RelationInputWrapper.js index 9020ea96dd..87b21142dd 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/RelationInputWrapper.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/RelationInputWrapper.js @@ -2,8 +2,9 @@ import PropTypes from 'prop-types'; import React, { memo, useMemo } from 'react'; import { useIntl } from 'react-intl'; -import { RelationInput, useCMEditViewDataManager, NotAllowedInput } from '@strapi/helper-plugin'; +import { useCMEditViewDataManager, NotAllowedInput } from '@strapi/helper-plugin'; +import { RelationInput } from '../RelationInput'; import { useRelation } from '../../hooks/useRelation'; import { connect, select, normalizeRelations } from './utils'; import { PUBLICATION_STATES } from './constants'; @@ -23,9 +24,10 @@ export const RelationInputWrapper = ({ targetModel, }) => { const { formatMessage } = useIntl(); - const { addRelation, removeRelation, loadRelation, modifiedData } = useCMEditViewDataManager(); + const { addRelation, removeRelation, loadRelation, modifiedData, slug, initialData } = + useCMEditViewDataManager(); - const { relations, search, searchFor } = useRelation(name, { + const { relations, search, searchFor } = useRelation(`${slug}-${name}-${initialData?.id ?? ''}`, { relation: { endpoint: endpoints.relation, onload(data) { @@ -105,35 +107,56 @@ export const RelationInputWrapper = ({ disabled={isDisabled} id={name} label={formatMessage(intlLabel)} - labelLoadMore={formatMessage({ + labelLoadMore={ + // TODO: only display if there are more; derive from count + !isCreatingEntry && + formatMessage({ + // TODO + id: 'tbd', + defaultMessage: 'Load More', + }) + } + loadingMessage={formatMessage({ + // TODO id: 'tbd', - defaultMessage: 'Load More', + defaultMessage: 'Relations are loading', })} name={name} - onRelationAdd={() => handleRelationAdd()} - onRelationRemove={() => handleRelationRemove()} + onRelationAdd={(relation) => handleRelationAdd(relation)} + onRelationRemove={(relation) => handleRelationRemove(relation)} onRelationLoadMore={() => handleRelationLoadMore()} - onSearch={() => handleSearch()} + onSearch={(term) => handleSearch(term)} onSearchNextPage={() => handleSearchMore()} + onSearchClose={() => {}} + onSearchOpen={() => {}} + placeholder={formatMessage({ + // TODO + id: 'tbd', + defaultMessage: 'Add relation', + })} publicationStateTranslations={{ [PUBLICATION_STATES.DRAFT]: formatMessage({ + // TODO id: 'tbd', defaultMessage: 'Draft', }), [PUBLICATION_STATES.PUBLISHED]: formatMessage({ + // TODO id: 'tbd', defaultMessage: 'Published', }), }} relations={normalizeRelations(relations, { - deletions: modifiedData?.[name], - mainFieldName: mainField.name, + modifiedData: modifiedData?.[name], + // TODO: Remove mock title + mainFieldName: 'title' || mainField.name, shouldAddLink: shouldDisplayRelationLink, targetModel, })} searchResults={normalizeRelations(search, { - mainFieldName: mainField.name, + // TODO: Remove mock title + mainFieldName: 'title' || mainField.name, })} /> ); diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/getRelationLink.js b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/getRelationLink.js index 9679b0c31e..97a5175107 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/getRelationLink.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/getRelationLink.js @@ -1,5 +1,5 @@ import { getRequestUrl } from '../../../utils'; export function getRelationLink(targetModel, id) { - return getRequestUrl(`collectionType/${targetModel}/${id ?? ''}`); + return `/admin${getRequestUrl(`collectionType/${targetModel}/${id ?? ''}`)}`; } diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/normalizeRelations.js b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/normalizeRelations.js index 0b91926ba9..08766ea7cd 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/normalizeRelations.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/normalizeRelations.js @@ -4,17 +4,23 @@ import { PUBLICATION_STATES } from '../constants'; export const normalizeRelations = ( relations, - { deletions = [], shouldAddLink = false, mainFieldName, targetModel } + { modifiedData = {}, shouldAddLink = false, mainFieldName, targetModel } ) => { + // TODO + if (!relations?.data?.pages) { + return relations; + } + return { + ...relations, data: { pages: relations.data.pages - .map((page) => - page + .map((page) => [ + ...page.values .map((relation) => { const nextRelation = { ...relation }; - if (deletions.find((deletion) => deletion.id === nextRelation.id)) { + if (modifiedData?.remove?.find((deletion) => deletion.id === nextRelation.id)) { return null; } @@ -34,8 +40,9 @@ export const normalizeRelations = ( return nextRelation; }) - .filter(Boolean) - ) + .filter(Boolean), + ...(modifiedData?.add ?? []), + ]) .filter((page) => page.length > 0), }, }; diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/select.js b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/select.js index 2cb62b3efa..de83b518ac 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/select.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/select.js @@ -52,7 +52,7 @@ function useSelect({ isUserAllowedToEditField, isUserAllowedToReadField, name, q ...queryInfos, endpoints: { ...queryInfos.endpoints, - fetch: relationFetchEndpoint, + relation: relationFetchEndpoint, }, }, isCreatingEntry, diff --git a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/tests/getRelationLink.test.js b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/tests/getRelationLink.test.js index a01ee3b450..0fe1ffc36d 100644 --- a/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/tests/getRelationLink.test.js +++ b/packages/core/admin/admin/src/content-manager/components/RelationInputWrapper/utils/tests/getRelationLink.test.js @@ -2,10 +2,12 @@ import { getRelationLink } from '../getRelationLink'; describe('getRelationLink', () => { test('returns an URL containing the targetModel and id', () => { - expect(getRelationLink('model', 2)).toBe('/content-manager/collectionType/model/2'); + expect(getRelationLink('model', 2)).toBe('/admin/content-manager/collectionType/model/2'); }); test('returns an URL containing the targetModel', () => { - expect(getRelationLink('model', undefined)).toBe('/content-manager/collectionType/model/'); + expect(getRelationLink('model', undefined)).toBe( + '/admin/content-manager/collectionType/model/' + ); }); }); diff --git a/packages/core/admin/admin/src/content-manager/hooks/useRelation/useRelation.js b/packages/core/admin/admin/src/content-manager/hooks/useRelation/useRelation.js index a3239b7e86..7950137312 100644 --- a/packages/core/admin/admin/src/content-manager/hooks/useRelation/useRelation.js +++ b/packages/core/admin/admin/src/content-manager/hooks/useRelation/useRelation.js @@ -1,34 +1,135 @@ import { useState } from 'react'; import { useInfiniteQuery } from 'react-query'; -import { axiosInstance } from '../../../core/utils'; +// import { axiosInstance } from '../../../core/utils'; -export const useRelation = (name, { relation, search }) => { +const FIXTURE_RELATIONS = { + values: [ + { + id: 1, + title: 'Relation 1', + publishedAt: '2022', + }, + + { + id: 2, + title: 'Relation 2', + publishedAt: '', + }, + + { + id: 3, + title: 'Relation 3', + }, + + { + id: 4, + title: 'Relation with a very long title', + }, + + { + id: 5, + title: 'Another important entity', + }, + + { + id: 6, + title: 'Are we going to play this game really?', + }, + + { + id: 7, + title: 'Indeed ...', + }, + ], + + pagination: { + page: 1, + total: 3, + }, +}; + +const FIXTURE_SEARCH = { + values: [ + { + id: 1, + title: 'Relation 1', + publishedAt: '2022', + }, + + { + id: 2, + title: 'Relation 2', + publishedAt: '', + }, + + { + id: 3, + title: 'Relation 3', + }, + + { + id: 4, + title: 'Relation with a very long title', + }, + + { + id: 5, + title: 'Another important entity', + }, + + { + id: 6, + title: 'Are we going to play this game really?', + }, + + { + id: 7, + title: 'Indeed ...', + }, + ], + + pagination: { + page: 1, + total: 1, + }, +}; + +export const useRelation = (cacheKey, { relation /* search */ }) => { const [searchTerm, setSearchTerm] = useState(null); - const fetchRelations = async ({ pageParam = 1 }) => { - const { data } = await axiosInstance.get(relation?.endpoint, { - ...(relation.pageParams ?? {}), - page: pageParam, - }); + const fetchRelations = async (/* { pageParam = 1 } */) => { + try { + // const { data } = await axiosInstance.get(relation?.endpoint, { + // ...(relation.pageParams ?? {}), + // page: pageParam, + // }); - if (relation?.onLoad) { - relation.onLoad(data); + if (relation?.onLoad) { + relation.onLoad(FIXTURE_RELATIONS); + // relation.onLoad(data); + } + + // TODO: remove + return FIXTURE_RELATIONS; + // return data; + } catch (err) { + // TODO: remove + return FIXTURE_RELATIONS; } - - return data; }; - const fetchSearch = async ({ pageParam = 1 }) => { - const { data } = await axiosInstance.get(search.endpoint, { - ...(search.pageParams ?? {}), - page: pageParam, - }); + const fetchSearch = async (/* { pageParam = 1 } */) => { + // const { data } = await axiosInstance.get(search.endpoint, { + // ...(search.pageParams ?? {}), + // page: pageParam, + // }); - return data; + return FIXTURE_SEARCH; + // return data; }; - const relationsRes = useInfiniteQuery(['relation', name], fetchRelations, { + const relationsRes = useInfiniteQuery(['relation', cacheKey], fetchRelations, { enabled: !!relation?.endpoint, getNextPageParam(lastPage) { if (lastPage.pagination.page + 1 === lastPage.pagination.total) { @@ -40,7 +141,7 @@ export const useRelation = (name, { relation, search }) => { }, }); - const searchRes = useInfiniteQuery(['relation', name, 'search', searchTerm], fetchSearch, { + const searchRes = useInfiniteQuery(['relation', cacheKey, 'search', searchTerm], fetchSearch, { enabled: !!searchTerm, getNextPageParam(lastPage) { if (lastPage.pagination.page + 1 === lastPage.pagination.total) {