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) {