@webbio/strapi-plugin-page-builder 0.7.0-platform → 0.8.0-platform
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/src/api/{page.ts → search-filtered-entity.ts} +37 -20
- package/admin/src/components/Combobox/index.tsx +8 -6
- package/admin/src/components/Combobox/styles.ts +13 -1
- package/admin/src/components/PageTypeEditView/PageByPlatformSelect/index.tsx +7 -7
- package/admin/src/components/PlatformFilteredSelectField/InputIcon/index.tsx +23 -0
- package/admin/src/components/PlatformFilteredSelectField/Multi/index.tsx +191 -0
- package/admin/src/components/PlatformFilteredSelectField/Single/index.tsx +184 -0
- package/admin/src/components/PlatformFilteredSelectField/hooks/useRelationLoad.tsx +124 -0
- package/admin/src/components/PlatformFilteredSelectField/index.tsx +43 -0
- package/admin/src/components/PlatformFilteredSelectField/styles.tsx +77 -0
- package/admin/src/components/PlatformFilteredSelectField/utils/get-translations.ts +3 -0
- package/admin/src/components/PlatformFilteredSelectField/utils/relation-helper.ts +76 -0
- package/admin/src/index.tsx +21 -0
- package/admin/src/translations/en.json +4 -1
- package/admin/src/translations/nl.json +4 -1
- package/admin/src/utils/getObjectFromFormName.ts +31 -0
- package/dist/package.json +3 -3
- package/dist/server/register.js +5 -0
- package/dist/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/register.ts +6 -0
- package/dist/server/graphql/page-by-slug.js +0 -82
- package/dist/server/utils/graphql.js +0 -100
- package/dist/server/utils/paginationValidation.js +0 -31
|
@@ -2,9 +2,8 @@ import { useQuery, UseQueryOptions } from 'react-query';
|
|
|
2
2
|
import orderBy from 'lodash/orderBy';
|
|
3
3
|
|
|
4
4
|
import { useFetchClient } from '@strapi/helper-plugin';
|
|
5
|
-
import { PAGE_UID } from '../../../shared/utils/constants';
|
|
6
5
|
|
|
7
|
-
export type
|
|
6
|
+
export type SearchFilteredEntitiesResult = {
|
|
8
7
|
pagination: {
|
|
9
8
|
page: number;
|
|
10
9
|
pageCount: number;
|
|
@@ -14,29 +13,33 @@ export type PageSearchResult = {
|
|
|
14
13
|
results: {
|
|
15
14
|
id: number;
|
|
16
15
|
title: string;
|
|
17
|
-
|
|
16
|
+
href: string;
|
|
17
|
+
publicationState?: string;
|
|
18
|
+
publishedAt?: string;
|
|
18
19
|
}[];
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
type
|
|
22
|
+
type SearchFilteredEntitiesQueryParams = {
|
|
22
23
|
fetchClient?: any;
|
|
24
|
+
uid: string;
|
|
23
25
|
page: number;
|
|
24
26
|
locale: string;
|
|
25
27
|
searchQuery?: string;
|
|
26
|
-
currentCollectionTypeId?: number;
|
|
27
28
|
platformTitle?: string;
|
|
29
|
+
notIds?: number[];
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
const QUERY_KEY = ['
|
|
32
|
+
const QUERY_KEY = ['filteredEntities'];
|
|
31
33
|
|
|
32
|
-
export const
|
|
34
|
+
export const getSearchFilteredEntities = async ({
|
|
33
35
|
fetchClient,
|
|
36
|
+
uid,
|
|
34
37
|
page,
|
|
35
38
|
locale,
|
|
36
39
|
searchQuery,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}:
|
|
40
|
+
platformTitle,
|
|
41
|
+
notIds
|
|
42
|
+
}: SearchFilteredEntitiesQueryParams): Promise<SearchFilteredEntitiesResult> => {
|
|
40
43
|
try {
|
|
41
44
|
const { get } = fetchClient;
|
|
42
45
|
const searchParams = new URLSearchParams();
|
|
@@ -54,13 +57,24 @@ export const getSearchPages = async ({
|
|
|
54
57
|
searchParams.append('filters[$and][0][platform][title][$contains]', String(platformTitle));
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
if (notIds && notIds.length > 0) {
|
|
61
|
+
for (let index = 0; index < notIds.length; index++) {
|
|
62
|
+
const id = notIds[index];
|
|
63
|
+
searchParams.append(`filters[$and][${index + 1}][id][$ne]`, String(id));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { data } = await get(`/content-manager/collection-types/${uid}?${searchParams.toString()}`);
|
|
58
68
|
|
|
59
|
-
const mapResults = data.results.map(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
const mapResults = data.results.map(
|
|
70
|
+
(result: Record<string, any>): SearchFilteredEntitiesResult['results'][number] => ({
|
|
71
|
+
id: result.id,
|
|
72
|
+
title: result.title,
|
|
73
|
+
publicationState: result?.publishedAt ? 'published' : 'draft',
|
|
74
|
+
publishedAt: result?.publishedAt,
|
|
75
|
+
href: `/content-manager/collectionType/${uid}/${result.id}`
|
|
76
|
+
})
|
|
77
|
+
);
|
|
64
78
|
|
|
65
79
|
return {
|
|
66
80
|
pagination: data.pagination,
|
|
@@ -74,18 +88,21 @@ export const getSearchPages = async ({
|
|
|
74
88
|
}
|
|
75
89
|
};
|
|
76
90
|
|
|
77
|
-
export const
|
|
91
|
+
export const useSearchFilteredEntities = (
|
|
92
|
+
params: SearchFilteredEntitiesQueryParams,
|
|
93
|
+
options?: UseQueryOptions<SearchFilteredEntitiesResult, Error>
|
|
94
|
+
) => {
|
|
78
95
|
const fetchClient = useFetchClient();
|
|
79
96
|
|
|
80
|
-
return useQuery<
|
|
97
|
+
return useQuery<SearchFilteredEntitiesResult, Error>(
|
|
81
98
|
[
|
|
82
|
-
QUERY_KEY,
|
|
99
|
+
[QUERY_KEY, params.uid],
|
|
83
100
|
{
|
|
84
101
|
...params
|
|
85
102
|
}
|
|
86
103
|
],
|
|
87
104
|
() =>
|
|
88
|
-
|
|
105
|
+
getSearchFilteredEntities({
|
|
89
106
|
...params,
|
|
90
107
|
fetchClient
|
|
91
108
|
}),
|
|
@@ -3,7 +3,7 @@ import { ClearIndicatorProps, DropdownIndicatorProps, GroupBase, components } fr
|
|
|
3
3
|
import AsyncSelect, { AsyncProps } from 'react-select/async';
|
|
4
4
|
|
|
5
5
|
import { CarretDown, Cross } from '@strapi/icons';
|
|
6
|
-
import {
|
|
6
|
+
import { FieldLabel } from '@strapi/design-system';
|
|
7
7
|
|
|
8
8
|
import S from './styles';
|
|
9
9
|
import { useReactSelectCustomStyles } from './react-select-custom-styles';
|
|
@@ -12,6 +12,8 @@ export interface IComboboxProps extends AsyncProps<IReactSelectValue, false, Gro
|
|
|
12
12
|
customOption?: typeof components.Option<IReactSelectValue, false, GroupBase<IReactSelectValue>>;
|
|
13
13
|
label?: string;
|
|
14
14
|
id: string;
|
|
15
|
+
extraLabelElement?: React.ReactNode;
|
|
16
|
+
labelAction?: React.ReactNode;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface IReactSelectValue {
|
|
@@ -21,7 +23,7 @@ export interface IReactSelectValue {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const Combobox = (props: IComboboxProps) => {
|
|
24
|
-
const { label, customOption, id, ...selectProps } = props;
|
|
26
|
+
const { label, customOption, extraLabelElement, labelAction, id, ...selectProps } = props;
|
|
25
27
|
const styles = useReactSelectCustomStyles();
|
|
26
28
|
const [inputValue, setInputValue] = useState<string | undefined>(props.inputValue);
|
|
27
29
|
|
|
@@ -32,9 +34,10 @@ const Combobox = (props: IComboboxProps) => {
|
|
|
32
34
|
return (
|
|
33
35
|
<S.Wrapper>
|
|
34
36
|
{props.label && (
|
|
35
|
-
<
|
|
36
|
-
{props.label}
|
|
37
|
-
|
|
37
|
+
<S.LabelWrapper>
|
|
38
|
+
<FieldLabel action={labelAction}>{props.label}</FieldLabel>
|
|
39
|
+
{extraLabelElement}
|
|
40
|
+
</S.LabelWrapper>
|
|
38
41
|
)}
|
|
39
42
|
<AsyncSelect
|
|
40
43
|
{...selectProps}
|
|
@@ -44,7 +47,6 @@ const Combobox = (props: IComboboxProps) => {
|
|
|
44
47
|
props.onInputChange?.(value, actionMeta);
|
|
45
48
|
setInputValue(value);
|
|
46
49
|
}}
|
|
47
|
-
// @ts-ignore
|
|
48
50
|
styles={styles}
|
|
49
51
|
inputValue={inputValue}
|
|
50
52
|
components={{
|
|
@@ -14,9 +14,21 @@ const Option = styled.span`
|
|
|
14
14
|
flex-direction: column;
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
|
+
const LabelWrapper = styled.div`
|
|
18
|
+
${({ theme }) => css`
|
|
19
|
+
width: 100%;
|
|
20
|
+
display: flex;
|
|
21
|
+
gap: 4px;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
color: ${theme.colors.neutral800};
|
|
24
|
+
font-size: ${theme.fontSizes[1]};
|
|
25
|
+
`}
|
|
26
|
+
`;
|
|
27
|
+
|
|
17
28
|
const ComboboxStyles = {
|
|
18
29
|
Wrapper,
|
|
19
|
-
Option
|
|
30
|
+
Option,
|
|
31
|
+
LabelWrapper
|
|
20
32
|
};
|
|
21
33
|
|
|
22
34
|
export default ComboboxStyles;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { useSelector } from 'react-redux';
|
|
3
3
|
import debounce from 'lodash/debounce';
|
|
4
|
-
import {
|
|
4
|
+
import { SingleValue } from 'react-select';
|
|
5
5
|
import { useCMEditViewDataManager, useFetchClient } from '@strapi/helper-plugin';
|
|
6
6
|
import { useGetLocaleFromUrl } from '../../../utils/hooks/useGetLocaleFromUrl';
|
|
7
7
|
import { IReactSelectValue, Combobox } from '../../Combobox';
|
|
8
|
-
import {
|
|
8
|
+
import { getSearchFilteredEntities } from '../../../api/search-filtered-entity';
|
|
9
|
+
import { PAGE_UID } from '../../../../../shared/utils/constants';
|
|
9
10
|
|
|
10
11
|
const SEARCH_DEBOUNCE_MS = 150;
|
|
11
12
|
const PAGE = 1;
|
|
@@ -61,19 +62,18 @@ export const PageByPlatformSearch = () => {
|
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
const getItems = async (inputValue?: string, platformTitle?: string): Promise<IReactSelectValue[]> => {
|
|
64
|
-
const pages = await
|
|
65
|
+
const pages = await getSearchFilteredEntities({
|
|
65
66
|
fetchClient,
|
|
66
67
|
page: PAGE,
|
|
68
|
+
uid: PAGE_UID,
|
|
67
69
|
locale: selectedLocale,
|
|
68
70
|
searchQuery: inputValue,
|
|
69
|
-
currentCollectionTypeId: initialData?.collectionTypeId,
|
|
70
71
|
platformTitle
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
return pages?.results.map((x) => ({
|
|
74
75
|
value: String(x.id),
|
|
75
|
-
label: x.title
|
|
76
|
-
initialSelected: x.isCurrentSelected
|
|
76
|
+
label: x.title
|
|
77
77
|
}));
|
|
78
78
|
};
|
|
79
79
|
|
|
@@ -90,7 +90,7 @@ export const PageByPlatformSearch = () => {
|
|
|
90
90
|
|
|
91
91
|
return (
|
|
92
92
|
<Combobox
|
|
93
|
-
key={`
|
|
93
|
+
key={`rerenderOnPlatformChange-${initialData?.platform?.[0]?.id}`}
|
|
94
94
|
id="pagesSearch"
|
|
95
95
|
label="Standaard overzichtspagina"
|
|
96
96
|
loadOptions={(i, c) => debouncedFetch(i, c, initialData?.platform?.[0]?.title)}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { Icon } from '@strapi/design-system';
|
|
4
|
+
import { Text } from '@strapi/icons';
|
|
5
|
+
|
|
6
|
+
const IconBox = styled.div`
|
|
7
|
+
svg > rect {
|
|
8
|
+
fill: #0e4dff;
|
|
9
|
+
stroke: #0e4dff;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
svg > path {
|
|
13
|
+
fill: #fff;
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const FieldIcon = () => (
|
|
18
|
+
<IconBox>
|
|
19
|
+
<Icon width={7} height={6} as={Text} />
|
|
20
|
+
</IconBox>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export default FieldIcon;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useIntl } from 'react-intl';
|
|
3
|
+
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|
4
|
+
import { useSelector } from 'react-redux';
|
|
5
|
+
import { useFetchClient } from '@strapi/helper-plugin';
|
|
6
|
+
import pick from 'lodash/pick';
|
|
7
|
+
|
|
8
|
+
import RelationInput from '@strapi/admin/admin/src/content-manager/components/RelationInput/RelationInput';
|
|
9
|
+
|
|
10
|
+
import { useGetLocaleFromUrl } from '../../../utils/hooks/useGetLocaleFromUrl';
|
|
11
|
+
import { getSearchFilteredEntities } from '../../../api/search-filtered-entity';
|
|
12
|
+
import RelationHelper, { IPlatformFilteredSelectFieldProps } from '../utils/relation-helper';
|
|
13
|
+
import { getTranslation } from '../utils/get-translations';
|
|
14
|
+
|
|
15
|
+
const PAGE = 1;
|
|
16
|
+
|
|
17
|
+
// This is largely a copy of https://github.com/strapi/strapi/blob/4e6961c7b8127f0f3bb0ad1fc430351ae9c4b8fa/packages/core/admin/admin/src/content-manager/components/Relations/RelationInputDataManager.tsx
|
|
18
|
+
|
|
19
|
+
export interface IMultiPlatformFilteredSelectFieldProps extends IPlatformFilteredSelectFieldProps {
|
|
20
|
+
relations?: any;
|
|
21
|
+
isCloningEntry?: boolean;
|
|
22
|
+
totalRelations: number;
|
|
23
|
+
relationsFromModifiedData?: Record<string, any>[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MultiPlatformFilteredSelectField = ({
|
|
27
|
+
attribute,
|
|
28
|
+
description,
|
|
29
|
+
labelAction,
|
|
30
|
+
error,
|
|
31
|
+
required,
|
|
32
|
+
placeholder,
|
|
33
|
+
intlLabel,
|
|
34
|
+
name,
|
|
35
|
+
relations,
|
|
36
|
+
isCloningEntry,
|
|
37
|
+
totalRelations,
|
|
38
|
+
relationsFromModifiedData
|
|
39
|
+
}: IMultiPlatformFilteredSelectFieldProps) => {
|
|
40
|
+
const fetchClient = useFetchClient();
|
|
41
|
+
const {
|
|
42
|
+
initialData,
|
|
43
|
+
modifiedData,
|
|
44
|
+
layout,
|
|
45
|
+
allLayoutData,
|
|
46
|
+
relationReorder,
|
|
47
|
+
relationDisconnect,
|
|
48
|
+
relationConnect,
|
|
49
|
+
isCreatingEntry
|
|
50
|
+
} = useCMEditViewDataManager() as any;
|
|
51
|
+
const { locales } = useSelector((state: any) => state.i18n_locales);
|
|
52
|
+
const urlLocale = useGetLocaleFromUrl();
|
|
53
|
+
const defaultLocale = locales.find((locale: any) => locale.isDefault);
|
|
54
|
+
const selectedLocale = initialData?.locale ?? urlLocale ?? defaultLocale.code;
|
|
55
|
+
|
|
56
|
+
const [searchResults, setSearchResults] = useState<Record<string, any>>({ data: [] });
|
|
57
|
+
const [selectedResults, setSelectedResults] = useState<Record<string, any>>({ data: [] });
|
|
58
|
+
|
|
59
|
+
const { formatMessage } = useIntl();
|
|
60
|
+
|
|
61
|
+
const { targetAttributes, targetFieldValue, targetField } = RelationHelper.getTargetAttributes(
|
|
62
|
+
name,
|
|
63
|
+
attribute,
|
|
64
|
+
modifiedData,
|
|
65
|
+
layout,
|
|
66
|
+
allLayoutData
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const label = intlLabel?.id
|
|
70
|
+
? formatMessage({ id: intlLabel.id, defaultMessage: intlLabel?.defaultMessage }, { ...intlLabel.values })
|
|
71
|
+
: name;
|
|
72
|
+
|
|
73
|
+
const hint = description?.id
|
|
74
|
+
? formatMessage({ id: description.id, defaultMessage: description.defaultMessage }, { ...description.values })
|
|
75
|
+
: '';
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
setSelectedResults({ data: targetFieldValue });
|
|
79
|
+
}, [targetFieldValue]);
|
|
80
|
+
|
|
81
|
+
const getItems = async (inputValue?: string) => {
|
|
82
|
+
const entities = await getSearchFilteredEntities({
|
|
83
|
+
fetchClient,
|
|
84
|
+
page: PAGE,
|
|
85
|
+
uid: targetAttributes?.target || '',
|
|
86
|
+
locale: selectedLocale,
|
|
87
|
+
searchQuery: inputValue,
|
|
88
|
+
platformTitle: initialData?.platform?.[0]?.title,
|
|
89
|
+
notIds: selectedResults?.data?.map((x) => x.id)
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const mapped = entities?.results.map((x) => ({
|
|
93
|
+
id: x.id,
|
|
94
|
+
mainField: x.title,
|
|
95
|
+
href: x?.href,
|
|
96
|
+
publicationState: x.publicationState,
|
|
97
|
+
publishedAt: x.publishedAt
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
setSearchResults({ data: mapped || [] });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleRelationReorder = (oldIndex: number, newIndex: number) => {
|
|
104
|
+
relationReorder?.({
|
|
105
|
+
name: targetField,
|
|
106
|
+
newIndex,
|
|
107
|
+
oldIndex
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleRelationLoadMore = () => {
|
|
112
|
+
relations.fetchNextPage();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleRelationConnect = (relation: Record<string, any>) => {
|
|
116
|
+
relationConnect?.({ name: targetField, value: relation, toOneRelation: false });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleRelationDisconnect = (relation: Record<string, any>) => {
|
|
120
|
+
relationDisconnect?.({ name: targetField, id: relation.id });
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<RelationInput
|
|
125
|
+
id={name}
|
|
126
|
+
name={name}
|
|
127
|
+
error={error}
|
|
128
|
+
canReorder
|
|
129
|
+
description={hint}
|
|
130
|
+
iconButtonAriaLabel="Drag"
|
|
131
|
+
label={`${label} ${totalRelations > 0 ? `(${totalRelations})` : ''}`}
|
|
132
|
+
labelLoadMore={
|
|
133
|
+
!isCreatingEntry || isCloningEntry
|
|
134
|
+
? formatMessage({
|
|
135
|
+
id: getTranslation('relation.loadMore'),
|
|
136
|
+
defaultMessage: 'Load More'
|
|
137
|
+
})
|
|
138
|
+
: undefined
|
|
139
|
+
}
|
|
140
|
+
liveText=""
|
|
141
|
+
listAriaDescription={formatMessage({
|
|
142
|
+
id: getTranslation('dnd.instructions'),
|
|
143
|
+
defaultMessage: `Press spacebar to grab and re-order`
|
|
144
|
+
})}
|
|
145
|
+
loadingMessage={formatMessage({
|
|
146
|
+
id: getTranslation('relation.isLoading'),
|
|
147
|
+
defaultMessage: 'Relations are loading'
|
|
148
|
+
})}
|
|
149
|
+
labelDisconnectRelation={formatMessage({
|
|
150
|
+
id: getTranslation('relation.disconnect'),
|
|
151
|
+
defaultMessage: 'Remove'
|
|
152
|
+
})}
|
|
153
|
+
numberOfRelationsToDisplay={5}
|
|
154
|
+
noRelationsMessage={formatMessage({
|
|
155
|
+
id: getTranslation('relation.notAvailable'),
|
|
156
|
+
defaultMessage: 'No relations available'
|
|
157
|
+
})}
|
|
158
|
+
onRelationConnect={handleRelationConnect}
|
|
159
|
+
onRelationDisconnect={handleRelationDisconnect}
|
|
160
|
+
onRelationLoadMore={handleRelationLoadMore}
|
|
161
|
+
onRelationReorder={handleRelationReorder}
|
|
162
|
+
onSearch={(e) => getItems(e)}
|
|
163
|
+
// onSearchNextPage={(e) => console.log(e)}
|
|
164
|
+
placeholder={formatMessage(
|
|
165
|
+
placeholder || {
|
|
166
|
+
id: getTranslation('relation.add'),
|
|
167
|
+
defaultMessage: 'Add relation'
|
|
168
|
+
}
|
|
169
|
+
)}
|
|
170
|
+
publicationStateTranslations={{
|
|
171
|
+
draft: 'Draft',
|
|
172
|
+
published: 'Published'
|
|
173
|
+
}}
|
|
174
|
+
relations={pick(
|
|
175
|
+
{ ...relations, data: relationsFromModifiedData },
|
|
176
|
+
'data',
|
|
177
|
+
'hasNextPage',
|
|
178
|
+
'isFetchingNextPage',
|
|
179
|
+
'isLoading',
|
|
180
|
+
'isSuccess'
|
|
181
|
+
)}
|
|
182
|
+
searchResults={searchResults}
|
|
183
|
+
size={8}
|
|
184
|
+
hint={hint}
|
|
185
|
+
labelAction={labelAction}
|
|
186
|
+
required={required}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default MultiPlatformFilteredSelectField;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useIntl } from 'react-intl';
|
|
3
|
+
import debounce from 'lodash/debounce';
|
|
4
|
+
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|
5
|
+
import { useSelector } from 'react-redux';
|
|
6
|
+
import { Field } from '@strapi/design-system';
|
|
7
|
+
import { useFetchClient } from '@strapi/helper-plugin';
|
|
8
|
+
import { Link } from '@strapi/icons';
|
|
9
|
+
|
|
10
|
+
import { Combobox, IReactSelectValue } from '../../Combobox';
|
|
11
|
+
import { OptionProps, SingleValue, components } from 'react-select';
|
|
12
|
+
import { useGetLocaleFromUrl } from '../../../utils/hooks/useGetLocaleFromUrl';
|
|
13
|
+
import { getSearchFilteredEntities } from '../../../api/search-filtered-entity';
|
|
14
|
+
import S from './../styles';
|
|
15
|
+
import getTrad from '../../../utils/getTrad';
|
|
16
|
+
import RelationHelper, { IPlatformFilteredSelectFieldProps } from '../utils/relation-helper';
|
|
17
|
+
|
|
18
|
+
interface CustomReactSelectValue extends Omit<IReactSelectValue, 'initialSelected'> {
|
|
19
|
+
href?: string;
|
|
20
|
+
publicationState?: string;
|
|
21
|
+
publishedAt?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SEARCH_DEBOUNCE_MS = 150;
|
|
25
|
+
const PAGE = 1;
|
|
26
|
+
|
|
27
|
+
const SinglePlatformFilteredSelectField = ({
|
|
28
|
+
attribute,
|
|
29
|
+
description,
|
|
30
|
+
labelAction,
|
|
31
|
+
error,
|
|
32
|
+
intlLabel,
|
|
33
|
+
name
|
|
34
|
+
}: IPlatformFilteredSelectFieldProps) => {
|
|
35
|
+
const fetchClient = useFetchClient();
|
|
36
|
+
const { onChange, initialData, modifiedData, isCreatingEntry, layout, allLayoutData } =
|
|
37
|
+
useCMEditViewDataManager() as any;
|
|
38
|
+
const { locales } = useSelector((state: any) => state.i18n_locales);
|
|
39
|
+
const urlLocale = useGetLocaleFromUrl();
|
|
40
|
+
const defaultLocale = locales.find((locale: any) => locale.isDefault);
|
|
41
|
+
const selectedLocale = initialData?.locale ?? urlLocale ?? defaultLocale.code;
|
|
42
|
+
|
|
43
|
+
const [selected, setSelected] = useState<SingleValue<CustomReactSelectValue | null>>();
|
|
44
|
+
|
|
45
|
+
const { formatMessage } = useIntl();
|
|
46
|
+
|
|
47
|
+
const { targetAttributes, targetFieldValue, targetField } = RelationHelper.getTargetAttributes(
|
|
48
|
+
name,
|
|
49
|
+
attribute,
|
|
50
|
+
modifiedData,
|
|
51
|
+
layout,
|
|
52
|
+
allLayoutData
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const label = intlLabel?.id
|
|
56
|
+
? formatMessage({ id: intlLabel.id, defaultMessage: intlLabel?.defaultMessage }, { ...intlLabel.values })
|
|
57
|
+
: name;
|
|
58
|
+
|
|
59
|
+
const hint = description?.id
|
|
60
|
+
? formatMessage({ id: description.id, defaultMessage: description.defaultMessage }, { ...description.values })
|
|
61
|
+
: '';
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const initialSelect = getInitialSelectItem(targetFieldValue?.[0]);
|
|
65
|
+
|
|
66
|
+
setSelected(initialSelect);
|
|
67
|
+
}, [targetFieldValue]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (isCreatingEntry) {
|
|
71
|
+
setSelected(null);
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const setFormValue = (name: string, value?: string | Record<string, any>[]) => {
|
|
76
|
+
onChange({
|
|
77
|
+
target: {
|
|
78
|
+
name,
|
|
79
|
+
value
|
|
80
|
+
},
|
|
81
|
+
shouldSetInitialValue: true
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleChange = (value?: SingleValue<CustomReactSelectValue>) => {
|
|
86
|
+
if (value) {
|
|
87
|
+
const newFormValue = {
|
|
88
|
+
...value,
|
|
89
|
+
id: value.value,
|
|
90
|
+
title: value.label,
|
|
91
|
+
mainField: value.label,
|
|
92
|
+
label: value.label,
|
|
93
|
+
value: value.value
|
|
94
|
+
};
|
|
95
|
+
setFormValue(targetField, [newFormValue]);
|
|
96
|
+
} else {
|
|
97
|
+
setFormValue(targetField, []);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const getItems = async (inputValue?: string, platformTitle?: string): Promise<CustomReactSelectValue[]> => {
|
|
102
|
+
const pages = await getSearchFilteredEntities({
|
|
103
|
+
fetchClient,
|
|
104
|
+
page: PAGE,
|
|
105
|
+
uid: targetAttributes?.target || '',
|
|
106
|
+
locale: selectedLocale,
|
|
107
|
+
searchQuery: inputValue,
|
|
108
|
+
platformTitle
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return pages?.results.map((x) => ({
|
|
112
|
+
value: String(x.id),
|
|
113
|
+
label: x.title,
|
|
114
|
+
href: x?.href,
|
|
115
|
+
publicationState: x.publicationState,
|
|
116
|
+
publishedAt: x.publishedAt
|
|
117
|
+
}));
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const debouncedFetch = debounce((searchTerm, callback, selectedPlatformTitle?: string) => {
|
|
121
|
+
promiseOptions(searchTerm, selectedPlatformTitle).then((result) => {
|
|
122
|
+
return callback(result || []);
|
|
123
|
+
});
|
|
124
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
125
|
+
|
|
126
|
+
const promiseOptions = (inputValue: string, selectedPlatformTitle?: string): Promise<CustomReactSelectValue[]> =>
|
|
127
|
+
new Promise<CustomReactSelectValue[]>((resolve) => {
|
|
128
|
+
resolve(getItems(inputValue, selectedPlatformTitle));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Field name={name} id={name} error={error} hint={hint}>
|
|
133
|
+
{targetAttributes?.type === 'relation' && (
|
|
134
|
+
<S.FieldWrapper>
|
|
135
|
+
<Combobox
|
|
136
|
+
key={`rerenderOnPlatformChange-${initialData?.platform?.[0]?.id}`}
|
|
137
|
+
id="pagesSearch"
|
|
138
|
+
label={label}
|
|
139
|
+
loadOptions={(i, c) => debouncedFetch(i, c, initialData?.platform?.[0]?.title)}
|
|
140
|
+
cacheOptions
|
|
141
|
+
customOption={CustomOption}
|
|
142
|
+
onChange={handleChange}
|
|
143
|
+
value={selected}
|
|
144
|
+
defaultOptions
|
|
145
|
+
hideSelectedOptions
|
|
146
|
+
labelAction={labelAction}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<S.LinkToPage
|
|
150
|
+
title={formatMessage({ id: getTrad('platformFilteredSelect.linkToEntity.label') })}
|
|
151
|
+
to={selected?.href}
|
|
152
|
+
disabled={!selected?.href}
|
|
153
|
+
>
|
|
154
|
+
<Link />
|
|
155
|
+
</S.LinkToPage>
|
|
156
|
+
</S.FieldWrapper>
|
|
157
|
+
)}
|
|
158
|
+
</Field>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const CustomOption = (props: OptionProps<CustomReactSelectValue, false>) => {
|
|
163
|
+
return (
|
|
164
|
+
<components.Option {...props}>
|
|
165
|
+
<S.CustomOption>
|
|
166
|
+
<S.CustomOptionStatus publicationState={props.data?.publicationState} />
|
|
167
|
+
{props.children}
|
|
168
|
+
</S.CustomOption>
|
|
169
|
+
</components.Option>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const getInitialSelectItem = (initialValue?: Record<string, any>): SingleValue<CustomReactSelectValue | null> =>
|
|
174
|
+
initialValue?.id
|
|
175
|
+
? {
|
|
176
|
+
value: String(initialValue?.id),
|
|
177
|
+
label: initialValue?.title ?? '',
|
|
178
|
+
href: initialValue?.href,
|
|
179
|
+
publicationState: initialValue.publicationState,
|
|
180
|
+
publishedAt: initialValue.publishedAt
|
|
181
|
+
}
|
|
182
|
+
: null;
|
|
183
|
+
|
|
184
|
+
export default SinglePlatformFilteredSelectField;
|