@webbio/strapi-plugin-page-builder 0.6.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/has-page-relation.ts +1 -1
- package/admin/src/api/has-platform-relation.ts +1 -1
- package/admin/src/api/platform-page-types.ts +17 -19
- package/admin/src/api/platform.ts +2 -2
- package/admin/src/api/search-filtered-entity.ts +111 -0
- package/admin/src/api/template.ts +10 -7
- package/admin/src/components/Combobox/index.tsx +8 -6
- package/admin/src/components/Combobox/styles.ts +13 -1
- package/admin/src/components/EditView/CollectionTypeSettings/CreatePageButton/index.tsx +6 -2
- package/admin/src/components/EditView/Details/index.tsx +1 -1
- package/admin/src/components/EditView/PageSettings/index.tsx +1 -1
- package/admin/src/components/EditView/Template/TemplateSelect/index.tsx +3 -2
- package/admin/src/components/EditView/Template/TemplateSelect/use-template-modules.ts +5 -2
- package/admin/src/components/EditView/page-type-select.tsx +1 -1
- package/admin/src/components/EditView/wrapper.tsx +13 -7
- package/admin/src/components/GlobalPlatformSelect/index.tsx +0 -1
- package/admin/src/components/PageFilters/PageTypeFilter/index.tsx +1 -1
- package/admin/src/components/PageFilters/filters.tsx +1 -1
- package/admin/src/components/PageFilters/index.tsx +6 -1
- package/admin/src/components/PageTypeEditView/PageByPlatformSelect/index.tsx +112 -0
- package/admin/src/components/PageTypeEditView/TemplatePlatformSelect/index.tsx +55 -0
- package/admin/src/components/PageTypeEditView/index.tsx +28 -0
- 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 +26 -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/admin/src/utils/hooks/usePlatformFormData.ts +1 -0
- package/dist/package.json +3 -3
- package/dist/server/controllers/page-type.js +6 -2
- package/dist/server/register.js +5 -0
- package/dist/server/routes/index.js +1 -1
- package/dist/server/schema/page-type-end.json +15 -5
- package/dist/server/schema/platform-start.json +0 -10
- package/dist/server/schema/template-end.json +40 -0
- package/dist/server/services/builder.js +4 -3
- package/dist/server/services/page-type.js +7 -3
- package/dist/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/controllers/page-type.ts +6 -1
- package/server/register.ts +6 -0
- package/server/routes/index.ts +1 -1
- package/server/schema/page-type-end.json +15 -5
- package/server/schema/platform-start.json +0 -10
- package/server/schema/template-end.json +40 -0
- package/server/services/builder.ts +4 -3
- package/server/services/page-type.ts +7 -3
- package/admin/src/api/page-type.ts +0 -32
- 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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|
4
|
+
import { SingleSelect, SingleSelectOption } from '@strapi/design-system';
|
|
5
|
+
|
|
6
|
+
import { useGetTemplates } from '../../../api/template';
|
|
7
|
+
|
|
8
|
+
export const TemplatePlatformSelect = () => {
|
|
9
|
+
const { onChange, modifiedData } = (useCMEditViewDataManager() || {}) as Record<string, any>;
|
|
10
|
+
const { data: templates } = useGetTemplates({ id: modifiedData?.platform?.[0]?.id });
|
|
11
|
+
|
|
12
|
+
const setFormValue = (name: string, value?: string | Record<string, any>[]) => {
|
|
13
|
+
onChange({
|
|
14
|
+
target: {
|
|
15
|
+
name,
|
|
16
|
+
value
|
|
17
|
+
},
|
|
18
|
+
shouldSetInitialValue: true
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handleSelectTemplate = (templateId: string) => {
|
|
23
|
+
const template = templates?.find((pageType) => pageType.id === Number(templateId));
|
|
24
|
+
|
|
25
|
+
if (template) {
|
|
26
|
+
const formTemplate = {
|
|
27
|
+
...template,
|
|
28
|
+
mainField: template.title,
|
|
29
|
+
label: template.title,
|
|
30
|
+
value: template.id
|
|
31
|
+
};
|
|
32
|
+
setFormValue('template', [formTemplate]);
|
|
33
|
+
} else {
|
|
34
|
+
setFormValue('template', []);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<SingleSelect
|
|
40
|
+
width="100%"
|
|
41
|
+
label="Template"
|
|
42
|
+
placeholder="Kies een template"
|
|
43
|
+
value={modifiedData?.template?.[0]?.id || 0}
|
|
44
|
+
onChange={handleSelectTemplate}
|
|
45
|
+
disabled={Boolean(!modifiedData?.platform?.[0]?.id)}
|
|
46
|
+
>
|
|
47
|
+
<SingleSelectOption value={0}>Geen template geselecteerd</SingleSelectOption>
|
|
48
|
+
{templates?.map((template) => (
|
|
49
|
+
<SingleSelectOption key={template.id} value={template.id}>
|
|
50
|
+
{template.title}
|
|
51
|
+
</SingleSelectOption>
|
|
52
|
+
))}
|
|
53
|
+
</SingleSelect>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|
4
|
+
import { Stack } from '@strapi/design-system';
|
|
5
|
+
|
|
6
|
+
import { PAGE_TYPE_UID } from '../../../../shared/utils/constants';
|
|
7
|
+
import { TemplatePlatformSelect } from './TemplatePlatformSelect';
|
|
8
|
+
import { Wrapper } from '../EditView/wrapper';
|
|
9
|
+
import { PageByPlatformSearch } from './PageByPlatformSelect';
|
|
10
|
+
|
|
11
|
+
export const PageTypeEditView = () => {
|
|
12
|
+
const { layout } = useCMEditViewDataManager() as any;
|
|
13
|
+
|
|
14
|
+
const isPageTypeCollectionType = layout.uid === PAGE_TYPE_UID;
|
|
15
|
+
|
|
16
|
+
if (isPageTypeCollectionType) {
|
|
17
|
+
return (
|
|
18
|
+
<Wrapper title="Instellingen">
|
|
19
|
+
<Stack spacing={4} width="100%">
|
|
20
|
+
<TemplatePlatformSelect />
|
|
21
|
+
<PageByPlatformSearch />
|
|
22
|
+
</Stack>
|
|
23
|
+
</Wrapper>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
@@ -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;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import get from 'lodash/get';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useRelation } from '@strapi/admin/admin/src/content-manager/hooks/useRelation/index';
|
|
4
|
+
import { getInitialDataPathUsingTempKeys } from '@strapi/admin/admin/src/content-manager/utils/paths';
|
|
5
|
+
import RelationHelper, { IPlatformFilteredSelectFieldProps } from '../utils/relation-helper';
|
|
6
|
+
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|
7
|
+
import { useRouteMatch } from 'react-router-dom';
|
|
8
|
+
|
|
9
|
+
const RELATIONS_TO_DISPLAY = 5;
|
|
10
|
+
const SEARCH_RESULTS_TO_DISPLAY = 10;
|
|
11
|
+
|
|
12
|
+
// This is largely a copy of https://github.com/strapi/strapi/blob/4e6961c7b8127f0f3bb0ad1fc430351ae9c4b8fa/packages/core/admin/admin/src/content-manager/components/Relations/RelationInputDataManager.tsx
|
|
13
|
+
|
|
14
|
+
const useRelationLoad = ({ name, attribute }: IPlatformFilteredSelectFieldProps) => {
|
|
15
|
+
const fieldName = name || '';
|
|
16
|
+
const { modifiedData, initialData, slug, layout, allLayoutData, isCreatingEntry, relationLoad } =
|
|
17
|
+
useCMEditViewDataManager() as any;
|
|
18
|
+
const { params } =
|
|
19
|
+
useRouteMatch<{ origin?: string }>('/content-manager/collectionType/:collectionType/create/clone/:origin') ?? {};
|
|
20
|
+
const { origin } = params ?? {};
|
|
21
|
+
const isCloningEntry = Boolean(origin);
|
|
22
|
+
const entityId = origin || modifiedData.id;
|
|
23
|
+
|
|
24
|
+
const { componentUid, componentId, relationMainFieldName, targetAttributes, targetField } =
|
|
25
|
+
RelationHelper.getTargetAttributes(fieldName, attribute, modifiedData, layout, allLayoutData);
|
|
26
|
+
|
|
27
|
+
const nameSplit = targetField.split('.');
|
|
28
|
+
const isComponentRelation = Boolean(componentUid);
|
|
29
|
+
const initialDataPath = getInitialDataPathUsingTempKeys(initialData, modifiedData)(targetField);
|
|
30
|
+
|
|
31
|
+
const currentLastPage = Math.ceil((get(initialData, targetField, []) || []).length / RELATIONS_TO_DISPLAY);
|
|
32
|
+
|
|
33
|
+
// /content-manager/relations/[model]/[id]/[field-name]
|
|
34
|
+
const relationFetchEndpoint = React.useMemo(() => {
|
|
35
|
+
if (isCreatingEntry && !origin) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (componentUid) {
|
|
40
|
+
// repeatable components and dz are dynamically created
|
|
41
|
+
// if no componentId exists in modifiedData it means that the user just created it
|
|
42
|
+
// there then are no relations to request
|
|
43
|
+
return componentId ? `/content-manager/relations/${componentUid}/${componentId}/${nameSplit.at(-1)}` : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `/content-manager/relations/${slug}/${entityId}/${targetField.split('.').at(-1)}`;
|
|
47
|
+
}, [isCreatingEntry, origin, componentUid, slug, entityId, targetField, componentId, nameSplit]);
|
|
48
|
+
|
|
49
|
+
// /content-manager/relations/[model]/[field-name]
|
|
50
|
+
const relationSearchEndpoint = React.useMemo(() => {
|
|
51
|
+
if (componentUid) {
|
|
52
|
+
return `/content-manager/relations/${componentUid}/${targetField.split('.').at(-1)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `/content-manager/relations/${slug}/${targetField.split('.').at(-1)}`;
|
|
56
|
+
}, [componentUid, slug, targetField]);
|
|
57
|
+
|
|
58
|
+
const { relations, search, searchFor } = useRelation([slug, initialDataPath.join('.'), modifiedData.id], {
|
|
59
|
+
relation: {
|
|
60
|
+
enabled: !!relationFetchEndpoint,
|
|
61
|
+
endpoint: relationFetchEndpoint!,
|
|
62
|
+
pageGoal: currentLastPage,
|
|
63
|
+
pageParams: {
|
|
64
|
+
pageSize: RELATIONS_TO_DISPLAY
|
|
65
|
+
},
|
|
66
|
+
onLoad(value) {
|
|
67
|
+
relationLoad?.({
|
|
68
|
+
target: {
|
|
69
|
+
initialDataPath: ['initialData', ...initialDataPath],
|
|
70
|
+
modifiedDataPath: ['modifiedData', ...nameSplit],
|
|
71
|
+
value
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
normalizeArguments: {
|
|
76
|
+
mainFieldName: relationMainFieldName,
|
|
77
|
+
shouldAddLink: true,
|
|
78
|
+
targetModel: targetAttributes?.targetModel
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
search: {
|
|
82
|
+
endpoint: relationSearchEndpoint,
|
|
83
|
+
pageParams: {
|
|
84
|
+
// eslint-disable-next-line no-nested-ternary
|
|
85
|
+
entityId: isCreatingEntry || isCloningEntry ? undefined : isComponentRelation ? componentId : entityId,
|
|
86
|
+
pageSize: SEARCH_RESULTS_TO_DISPLAY
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* How to calculate the total number of relations even if you don't
|
|
93
|
+
* have them all loaded in the browser.
|
|
94
|
+
*
|
|
95
|
+
* 1. The `infiniteQuery` gives you the total number of relations in the pagination result.
|
|
96
|
+
* 2. You can diff the length of the browserState vs the fetchedServerState to determine if you've
|
|
97
|
+
* either added or removed relations.
|
|
98
|
+
* 3. Add them together, if you've removed relations you'll get a negative number and it'll
|
|
99
|
+
* actually subtract from the total number on the server (regardless of how many you fetched).
|
|
100
|
+
*/
|
|
101
|
+
const relationsFromModifiedData = get(modifiedData, targetField, []);
|
|
102
|
+
const browserRelationsCount = (relationsFromModifiedData || [])?.length;
|
|
103
|
+
const serverRelationsCount = (get(initialData, initialDataPath) ?? []).length;
|
|
104
|
+
const realServerRelationsCount = relations.data?.pages[0]?.pagination?.total ?? 0;
|
|
105
|
+
/**
|
|
106
|
+
* _IF_ theres no relations data and the browserCount is the same as serverCount you can therefore assume
|
|
107
|
+
* that the browser count is correct because we've just _made_ this entry and the in-component hook is now fetching.
|
|
108
|
+
*/
|
|
109
|
+
const totalRelations =
|
|
110
|
+
!relations.data && browserRelationsCount === serverRelationsCount
|
|
111
|
+
? browserRelationsCount
|
|
112
|
+
: browserRelationsCount - serverRelationsCount + realServerRelationsCount;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
relations,
|
|
116
|
+
search,
|
|
117
|
+
searchFor,
|
|
118
|
+
isCloningEntry,
|
|
119
|
+
relationsFromModifiedData,
|
|
120
|
+
totalRelations: totalRelations || 0
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export { useRelationLoad };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|
3
|
+
|
|
4
|
+
import RelationHelper, { IPlatformFilteredSelectFieldProps } from './utils/relation-helper';
|
|
5
|
+
import MultiPlatformFilteredSelectField from './Multi';
|
|
6
|
+
import SinglePlatformFilteredSelectField from './Single';
|
|
7
|
+
import { useRelationLoad } from './hooks/useRelationLoad';
|
|
8
|
+
|
|
9
|
+
const PlatformFilteredSelectField = (props: IPlatformFilteredSelectFieldProps) => {
|
|
10
|
+
const { name, attribute } = props;
|
|
11
|
+
const { modifiedData, layout, allLayoutData } = useCMEditViewDataManager() as any;
|
|
12
|
+
const { relations, isCloningEntry, totalRelations, relationsFromModifiedData } = useRelationLoad(props);
|
|
13
|
+
const { targetAttributes } = RelationHelper.getTargetAttributes(name, attribute, modifiedData, layout, allLayoutData);
|
|
14
|
+
const toOneRelation = ['oneWay', 'oneToOne', 'manyToOne', 'oneToManyMorph', 'oneToOneMorph'].includes(
|
|
15
|
+
targetAttributes?.relation || ''
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
if (toOneRelation) {
|
|
19
|
+
return <SinglePlatformFilteredSelectField {...props} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<MultiPlatformFilteredSelectField
|
|
24
|
+
relations={relations}
|
|
25
|
+
isCloningEntry={isCloningEntry}
|
|
26
|
+
totalRelations={totalRelations}
|
|
27
|
+
relationsFromModifiedData={relationsFromModifiedData}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
PlatformFilteredSelectField.defaultProps = {
|
|
34
|
+
description: undefined,
|
|
35
|
+
disabled: false,
|
|
36
|
+
error: undefined,
|
|
37
|
+
labelAction: undefined,
|
|
38
|
+
placeholder: undefined,
|
|
39
|
+
value: '',
|
|
40
|
+
required: false
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default PlatformFilteredSelectField;
|