@webbio/strapi-plugin-page-builder 0.0.1

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.
Files changed (75) hide show
  1. package/.prettierignore +17 -0
  2. package/README.md +3 -0
  3. package/admin/src/api/collection-type.ts +110 -0
  4. package/admin/src/api/has-page-relation.ts +34 -0
  5. package/admin/src/api/page-type.ts +31 -0
  6. package/admin/src/api/template.ts +25 -0
  7. package/admin/src/components/Combobox/index.tsx +77 -0
  8. package/admin/src/components/Combobox/react-select-custom-styles.tsx +111 -0
  9. package/admin/src/components/Combobox/styles.ts +22 -0
  10. package/admin/src/components/ConfirmModal/index.tsx +90 -0
  11. package/admin/src/components/EditView/CollectionTypeSearch/index.tsx +118 -0
  12. package/admin/src/components/EditView/CollectionTypeSettings/CreatePageButton/index.tsx +95 -0
  13. package/admin/src/components/EditView/CollectionTypeSettings/CreatePageButton/styles.ts +26 -0
  14. package/admin/src/components/EditView/CollectionTypeSettings/index.tsx +53 -0
  15. package/admin/src/components/EditView/Details/index.tsx +47 -0
  16. package/admin/src/components/EditView/Details/styles.ts +51 -0
  17. package/admin/src/components/EditView/PageSettings/index.tsx +104 -0
  18. package/admin/src/components/EditView/Template/TemplateConfirmModal/index.tsx +36 -0
  19. package/admin/src/components/EditView/Template/TemplateSelect/index.tsx +64 -0
  20. package/admin/src/components/EditView/Template/TemplateSelect/use-template-modules.ts +30 -0
  21. package/admin/src/components/EditView/index.tsx +27 -0
  22. package/admin/src/components/EditView/page-type-select.tsx +30 -0
  23. package/admin/src/components/EditView/wrapper.tsx +35 -0
  24. package/admin/src/components/Initializer/index.tsx +24 -0
  25. package/admin/src/components/PageTypeFilter/index.tsx +17 -0
  26. package/admin/src/components/PageTypeFilter/page-type-filter.tsx +130 -0
  27. package/admin/src/components/PluginIcon/index.tsx +12 -0
  28. package/admin/src/constants.ts +1 -0
  29. package/admin/src/index.tsx +115 -0
  30. package/admin/src/pages/App/index.tsx +25 -0
  31. package/admin/src/pages/HomePage/index.tsx +19 -0
  32. package/admin/src/pluginId.ts +5 -0
  33. package/admin/src/redux/initialData.reducer.ts +0 -0
  34. package/admin/src/translations/en.json +6 -0
  35. package/admin/src/translations/nl.json +6 -0
  36. package/admin/src/utils/getRequestUrl.ts +11 -0
  37. package/admin/src/utils/getTrad.ts +5 -0
  38. package/admin/src/utils/hooks/useDebounce.ts +17 -0
  39. package/admin/src/utils/hooks/useGetLocaleFromUrl.ts +9 -0
  40. package/admin/src/utils/hooks/usePrevious.ts +12 -0
  41. package/admin/src/utils/sanitizeModules.ts +10 -0
  42. package/custom.d.ts +5 -0
  43. package/package.json +71 -0
  44. package/server/bootstrap.ts +106 -0
  45. package/server/config/index.ts +4 -0
  46. package/server/content-types/index.ts +1 -0
  47. package/server/controllers/collection-types.ts +27 -0
  48. package/server/controllers/index.ts +9 -0
  49. package/server/controllers/page-type.ts +12 -0
  50. package/server/controllers/page.ts +20 -0
  51. package/server/destroy.ts +5 -0
  52. package/server/index.ts +23 -0
  53. package/server/middlewares/index.ts +1 -0
  54. package/server/policies/index.ts +1 -0
  55. package/server/register.ts +5 -0
  56. package/server/routes/index.ts +42 -0
  57. package/server/schema/page-end.json +97 -0
  58. package/server/schema/page-start.json +87 -0
  59. package/server/schema/page-type-end.json +49 -0
  60. package/server/schema/page-type-start.json +44 -0
  61. package/server/schema/template.json +35 -0
  62. package/server/services/builder.ts +121 -0
  63. package/server/services/collection-types.ts +49 -0
  64. package/server/services/index.ts +11 -0
  65. package/server/services/page-type.ts +22 -0
  66. package/server/services/page.ts +24 -0
  67. package/server/utils/graphql.ts +110 -0
  68. package/server/utils/reload-strapi-on-load.ts +13 -0
  69. package/shared/utils/constants.ts +4 -0
  70. package/shared/utils/sleep.ts +1 -0
  71. package/strapi-admin.js +3 -0
  72. package/strapi-server.js +3 -0
  73. package/tsconfig.json +20 -0
  74. package/tsconfig.server.json +25 -0
  75. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,17 @@
1
+ .cache
2
+ .husky
3
+ build
4
+ node_modules
5
+ public
6
+ .dockerignore
7
+ .editorconfig
8
+ .env.example
9
+ .eslintignore
10
+ .gitignore
11
+ .gitmodules
12
+ .prettierignore
13
+ .strapi-updater.json
14
+ favicon.ico
15
+ yarn.lock
16
+ Dockerfile
17
+ *.patch
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Strapi plugin page-builder
2
+
3
+ A quick description of page-builder.
@@ -0,0 +1,110 @@
1
+ import { useQuery, UseQueryOptions } from 'react-query';
2
+ import orderBy from 'lodash/orderBy';
3
+
4
+ import { useFetchClient } from '@strapi/helper-plugin';
5
+
6
+ export type SearchResult = {
7
+ pagination: {
8
+ page: number;
9
+ pageCount: number;
10
+ pageSize: number;
11
+ total: number;
12
+ };
13
+ results: {
14
+ id: number;
15
+ title: string;
16
+ hasPages?: boolean;
17
+ isCurrentSelected?: boolean;
18
+ }[];
19
+ };
20
+
21
+ type SearchEntitiesQueryParams = {
22
+ fetchClient?: any;
23
+ uid: string;
24
+ page: number;
25
+ locale: string;
26
+ searchQuery?: string;
27
+ currentCollectionTypeId?: number;
28
+ currentPageId?: number;
29
+ };
30
+
31
+ const QUERY_KEY = ['entities'];
32
+
33
+ export const getSearchEntities = async ({
34
+ fetchClient,
35
+ uid,
36
+ page,
37
+ locale,
38
+ searchQuery,
39
+ currentCollectionTypeId,
40
+ currentPageId
41
+ }: SearchEntitiesQueryParams): Promise<SearchResult> => {
42
+ try {
43
+ const { get } = fetchClient;
44
+ const searchParams = new URLSearchParams();
45
+ searchParams.append('page', String(page));
46
+ searchParams.append('pageSize', '20');
47
+ searchParams.append('sort', 'page[id]:DESC');
48
+ searchParams.append('locale', locale);
49
+
50
+ if (searchQuery) {
51
+ searchParams.delete('sort');
52
+ searchParams.append('sort', 'title:ASC');
53
+ searchParams.append('_q', searchQuery);
54
+ }
55
+
56
+ const { data } = await get(`/content-manager/collection-types/${uid}?${searchParams.toString()}`);
57
+
58
+ const filteredResults = data.results.filter((p: Record<string, any>) => {
59
+ // Don't return entities with connected pages when no searchquery is set, unless the entity is the initial selected one.
60
+ if (!searchQuery) {
61
+ return p?.page === null || p.id === currentCollectionTypeId;
62
+ }
63
+
64
+ return true;
65
+ });
66
+
67
+ const mapResults = filteredResults.map((result: Record<string, any>) => {
68
+ const resultPages = (result?.page || []).filter((x: Record<string, any>) => x.id !== currentPageId);
69
+
70
+ return {
71
+ id: result.id,
72
+ title: result.title,
73
+ hasPages: resultPages.length > 0,
74
+ isCurrentSelected: result?.id === currentCollectionTypeId
75
+ };
76
+ });
77
+
78
+ return {
79
+ pagination: data.pagination,
80
+ results: orderBy(mapResults, ['hasPages', 'title'], ['asc', 'asc'])
81
+ };
82
+ } catch (e) {
83
+ return {
84
+ pagination: { page: 1, pageCount: 0, pageSize: 0, total: 0 },
85
+ results: []
86
+ };
87
+ }
88
+ };
89
+
90
+ export const useSearchEntities = (
91
+ params: SearchEntitiesQueryParams,
92
+ options?: UseQueryOptions<SearchResult, Error>
93
+ ) => {
94
+ const fetchClient = useFetchClient();
95
+
96
+ return useQuery<SearchResult, Error>(
97
+ [
98
+ QUERY_KEY,
99
+ {
100
+ ...params
101
+ }
102
+ ],
103
+ () =>
104
+ getSearchEntities({
105
+ ...params,
106
+ fetchClient
107
+ }),
108
+ options
109
+ );
110
+ };
@@ -0,0 +1,34 @@
1
+ import { useQuery, UseQueryOptions } from 'react-query';
2
+
3
+ import { useFetchClient } from '@strapi/helper-plugin';
4
+
5
+ import getRequestUrl from '../utils/getRequestUrl';
6
+
7
+ type HasPageRelationQueryParams = {
8
+ uid: string;
9
+ fetchClient?: any;
10
+ };
11
+
12
+ const QUERY_KEY = ['pageRelation'];
13
+
14
+ const fetchHasPageRelation = async ({ fetchClient, uid }: HasPageRelationQueryParams): Promise<boolean> => {
15
+ const { get } = fetchClient;
16
+ const result = await get(`${getRequestUrl('collection-types')}/${uid}`);
17
+
18
+ return Boolean(result?.data?.hasPageRelation);
19
+ };
20
+
21
+ export const useHasPageRelation = (params: HasPageRelationQueryParams, options?: UseQueryOptions<boolean, Error>) => {
22
+ const fetchClient = useFetchClient();
23
+
24
+ return useQuery<boolean, Error>(
25
+ [
26
+ QUERY_KEY,
27
+ {
28
+ ...params
29
+ }
30
+ ],
31
+ () => fetchHasPageRelation({ ...params, fetchClient }),
32
+ options
33
+ );
34
+ };
@@ -0,0 +1,31 @@
1
+ import { useQuery } from 'react-query';
2
+ import { useFetchClient } from '@strapi/helper-plugin';
3
+
4
+ export type PageType = {
5
+ id: number;
6
+ uid: string;
7
+ title?: string;
8
+ };
9
+
10
+ const QUERY_KEY = 'page-types';
11
+
12
+ const fetchPageTypes = async ({ fetchClient }: any): Promise<PageType[]> => {
13
+ const { get } = fetchClient;
14
+ const result = await get(`/content-manager/collection-types/api::page-type.page-type`);
15
+
16
+ return result?.data?.results?.map((entity: any) => ({
17
+ id: entity.id,
18
+ uid: entity.uid,
19
+ title: entity.title,
20
+ templateId: entity?.template?.id
21
+ }));
22
+ };
23
+
24
+ export const useGetPageTypes = (params: any) => {
25
+ const fetchClient = useFetchClient();
26
+ params = {
27
+ ...params,
28
+ fetchClient
29
+ };
30
+ return useQuery<PageType[], Error>(QUERY_KEY, () => fetchPageTypes(params));
31
+ };
@@ -0,0 +1,25 @@
1
+ import { useQuery } from 'react-query';
2
+ import { useFetchClient } from '@strapi/helper-plugin';
3
+
4
+ export type Template = {
5
+ id: number;
6
+ title: string;
7
+ modules: any[];
8
+ };
9
+
10
+ const QUERY_KEY = ['templates'];
11
+
12
+ const fetchTemplates = async ({ fetchClient }: any): Promise<Template[]> => {
13
+ const { get } = fetchClient;
14
+ const result = await get(`/content-manager/collection-types/api::template.template`);
15
+ return result?.data?.results;
16
+ };
17
+
18
+ export const useGetTemplates = (params: any) => {
19
+ const fetchClient = useFetchClient();
20
+ params = {
21
+ ...params,
22
+ fetchClient
23
+ };
24
+ return useQuery<Template[], Error>(QUERY_KEY, () => fetchTemplates(params));
25
+ };
@@ -0,0 +1,77 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ClearIndicatorProps, DropdownIndicatorProps, GroupBase, components } from 'react-select';
3
+ import AsyncSelect, { AsyncProps } from 'react-select/async';
4
+
5
+ import { CarretDown, Cross } from '@strapi/icons';
6
+ import { Typography } from '@strapi/design-system';
7
+
8
+ import S from './styles';
9
+ import { useReactSelectCustomStyles } from './react-select-custom-styles';
10
+
11
+ export interface IComboboxProps extends AsyncProps<IReactSelectValue, false, GroupBase<IReactSelectValue>> {
12
+ customOption?: typeof components.Option<IReactSelectValue, false, GroupBase<IReactSelectValue>>;
13
+ label?: string;
14
+ id: string;
15
+ }
16
+
17
+ export interface IReactSelectValue {
18
+ value: string;
19
+ label: string;
20
+ initialSelected?: boolean;
21
+ }
22
+
23
+ const Combobox = (props: IComboboxProps) => {
24
+ const { label, customOption, id, ...selectProps } = props;
25
+ const styles = useReactSelectCustomStyles();
26
+ const [inputValue, setInputValue] = useState<string | undefined>(props.inputValue);
27
+
28
+ useEffect(() => {
29
+ setInputValue(props.inputValue);
30
+ }, [props.inputValue]);
31
+
32
+ return (
33
+ <S.Wrapper>
34
+ {props.label && (
35
+ <Typography as="label" htmlFor={id} variant="pi" fontWeight="bold" textColor="neutral800">
36
+ {props.label}
37
+ </Typography>
38
+ )}
39
+ <AsyncSelect
40
+ {...selectProps}
41
+ inputId={id}
42
+ isClearable
43
+ onInputChange={(value, actionMeta) => {
44
+ props.onInputChange?.(value, actionMeta);
45
+ setInputValue(value);
46
+ }}
47
+ // @ts-ignore
48
+ styles={styles}
49
+ inputValue={inputValue}
50
+ components={{
51
+ IndicatorSeparator: null,
52
+ ClearIndicator,
53
+ DropdownIndicator,
54
+ Option: props.customOption || components.Option
55
+ }}
56
+ />
57
+ </S.Wrapper>
58
+ );
59
+ };
60
+
61
+ export { Combobox };
62
+
63
+ const ClearIndicator = (props: ClearIndicatorProps<IReactSelectValue, false>) => {
64
+ return (
65
+ <components.ClearIndicator {...props}>
66
+ <Cross />
67
+ </components.ClearIndicator>
68
+ );
69
+ };
70
+
71
+ const DropdownIndicator = (props: DropdownIndicatorProps<IReactSelectValue, false>) => {
72
+ return (
73
+ <components.DropdownIndicator {...props}>
74
+ <CarretDown />
75
+ </components.DropdownIndicator>
76
+ );
77
+ };
@@ -0,0 +1,111 @@
1
+ import { StylesConfig } from 'react-select';
2
+ import { useTheme } from 'styled-components';
3
+ import { IReactSelectValue } from '.';
4
+
5
+ export const useReactSelectCustomStyles = (): StylesConfig<IReactSelectValue, false> => {
6
+ const theme = useTheme() as any;
7
+
8
+ return {
9
+ control: (provided, { isFocused, isDisabled }) => ({
10
+ ...provided,
11
+ color: theme.colors.neutral800,
12
+ backgroundColor: provided.backgroundColor,
13
+ minHeight: '40px',
14
+ lineHeight: 1.4,
15
+ borderRadius: theme.borderRadius,
16
+ fontSize: theme.fontSizes[2],
17
+ borderColor: isFocused ? theme.colors.buttonPrimary600 : theme.colors.neutral200,
18
+ boxShadow: isFocused ? `${theme.colors.buttonPrimary600} 0px 0px 0px 2px` : 'none',
19
+
20
+ ':hover': {
21
+ borderColor: isFocused ? theme.colors.buttonPrimary600 : theme.colors.neutral200
22
+ }
23
+ }),
24
+ placeholder: (provided) => ({
25
+ ...provided,
26
+ color: theme.colors.neutral500
27
+ }),
28
+ menu: (provided) => ({
29
+ ...provided,
30
+ border: `1px solid ${theme.colors.neutral150}`,
31
+ boxShadow: '0px 1px 4px rgba(33, 33, 52, 0.1)',
32
+ borderRadius: theme.borderRadius,
33
+ color: theme.colors.neutral800
34
+ }),
35
+ menuList: (provided) => ({
36
+ ...provided,
37
+ paddingLeft: '4px',
38
+ paddingRight: '4px'
39
+ }),
40
+ option: (provided, { isFocused, isSelected, isDisabled }) => ({
41
+ ...provided,
42
+ backgroundColor: isFocused ? theme.colors.primary100 : 'transparent',
43
+ fontSize: theme.fontSizes[2],
44
+ borderRadius: theme.borderRadius,
45
+ color: isSelected ? theme.colors.buttonPrimary600 : theme.colors.neutral800,
46
+ fontWeight: isSelected ? 700 : 400,
47
+ minHeight: '40px',
48
+ display: 'flex',
49
+ justifyContent: 'center',
50
+ flexDirection: 'column',
51
+ gap: '4px',
52
+ opacity: isDisabled ? 0.7 : 1,
53
+
54
+ 'span:not(:first-of-type)': {
55
+ fontSize: theme.fontSizes[1],
56
+ color: theme.colors.neutral500
57
+ },
58
+
59
+ '&:active': {
60
+ backgroundColor: theme.colors.primary100
61
+ }
62
+ }),
63
+ loadingIndicator: (provided) => ({
64
+ ...provided,
65
+ '>span': {
66
+ backgroundColor: theme.colors.buttonPrimary600
67
+ }
68
+ }),
69
+ loadingMessage: (provided) => ({
70
+ ...provided,
71
+ color: theme.colors.neutral500
72
+ }),
73
+ noOptionsMessage: (provided) => ({
74
+ ...provided,
75
+ color: theme.colors.neutral500
76
+ }),
77
+ clearIndicator: (provided, { isFocused }) => ({
78
+ ...provided,
79
+ cursor: 'pointer',
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
83
+ paddingLeft: '6px',
84
+ paddingRight: '6px',
85
+ svg: {
86
+ width: '0.6875rem',
87
+ path: {
88
+ fill: theme.colors.neutral600
89
+ }
90
+ },
91
+ ':hover svg path': {
92
+ fill: theme.colors.neutral700
93
+ }
94
+ }),
95
+
96
+ dropdownIndicator: (provided, { isFocused }) => ({
97
+ ...provided,
98
+ display: 'flex',
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ paddingLeft: '6px',
102
+ paddingRight: '12px',
103
+ svg: {
104
+ width: '0.375rem',
105
+ path: {
106
+ fill: theme.colors.neutral600
107
+ }
108
+ }
109
+ })
110
+ };
111
+ };
@@ -0,0 +1,22 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ const Wrapper = styled.div`
4
+ ${({ theme }) => css`
5
+ width: 100%;
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 4px;
9
+ `}
10
+ `;
11
+
12
+ const Option = styled.span`
13
+ display: flex;
14
+ flex-direction: column;
15
+ `;
16
+
17
+ const ComboboxStyles = {
18
+ Wrapper,
19
+ Option
20
+ };
21
+
22
+ export default ComboboxStyles;
@@ -0,0 +1,90 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+
4
+ import { ModalLayout, ModalBody, ModalHeader, ModalFooter, Button, Typography } from '@strapi/design-system';
5
+
6
+ import getTrad from '../../utils/getTrad';
7
+ import { sleep } from '../../../../shared/utils/sleep';
8
+
9
+ export interface IConfirmModalProps {
10
+ onSubmit: () => void;
11
+ closeModal: () => void;
12
+ isOpen?: boolean;
13
+ title?: React.ReactNode;
14
+ body?: React.ReactNode;
15
+ submitText?: string;
16
+ }
17
+
18
+ const ConfirmModal = ({
19
+ isOpen,
20
+ title,
21
+ body,
22
+ submitText,
23
+ onSubmit = () => {
24
+ console.warn('Modal submit function not set');
25
+ },
26
+ closeModal = () => {
27
+ console.warn('Modal close function not set');
28
+ }
29
+ }: IConfirmModalProps): JSX.Element => {
30
+ const { formatMessage } = useIntl();
31
+
32
+ const colors = {
33
+ text: 'neutral800',
34
+ icon: 'neutral900'
35
+ };
36
+
37
+ const fixModalFocus = async () => {
38
+ if (isOpen) {
39
+ // To make sure the modal's focus trap works, we need to wait a short time before blurring the document
40
+ await sleep(100);
41
+ // @ts-ignore
42
+ document?.activeElement?.blur();
43
+ }
44
+ };
45
+
46
+ useEffect(() => {
47
+ fixModalFocus();
48
+ }, [isOpen]);
49
+
50
+ if (!isOpen) {
51
+ return <></>;
52
+ }
53
+
54
+ return (
55
+ <ModalLayout onClose={closeModal} labelledBy="title" width="500px">
56
+ <ModalHeader>
57
+ <Typography fontWeight="bold" textColor={colors.text} as="h2" id="title">
58
+ {title}
59
+ </Typography>
60
+ </ModalHeader>
61
+
62
+ {/* New Strapi design system overflow styling breaks dropdowns in modals */}
63
+ <ModalBody style={{ overflow: 'initial' }}>
64
+ <Typography textColor={colors.text} id="body">
65
+ {body}
66
+ </Typography>
67
+ </ModalBody>
68
+
69
+ <ModalFooter
70
+ startActions={
71
+ <Button onClick={closeModal} variant="tertiary">
72
+ {formatMessage({
73
+ id: getTrad('template.confirmModal.buttons.cancel')
74
+ })}
75
+ </Button>
76
+ }
77
+ endActions={
78
+ <Button onClick={onSubmit}>
79
+ {submitText ||
80
+ formatMessage({
81
+ id: getTrad('template.confirmModal.buttons.submit')
82
+ })}
83
+ </Button>
84
+ }
85
+ />
86
+ </ModalLayout>
87
+ );
88
+ };
89
+
90
+ export default ConfirmModal;
@@ -0,0 +1,118 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import debounce from 'lodash/debounce';
4
+ import { OptionProps, SingleValue, components } from 'react-select';
5
+ import { useCMEditViewDataManager, useFetchClient } from '@strapi/helper-plugin';
6
+
7
+ import { Combobox, IReactSelectValue } from '../../Combobox';
8
+ import { getSearchEntities } from '../../../api/collection-type';
9
+ import { usePrevious } from '../../../utils/hooks/usePrevious';
10
+ import { useGetLocaleFromUrl } from '../../../utils/hooks/useGetLocaleFromUrl';
11
+
12
+ const SEARCH_DEBOUNCE_MS = 150;
13
+ const PAGE = 1;
14
+
15
+ interface Props {
16
+ uid: string;
17
+ }
18
+
19
+ export const CollectionTypeSearch = ({ uid }: Props) => {
20
+ const fetchClient = useFetchClient();
21
+ const form = useCMEditViewDataManager();
22
+ const { locales } = useSelector((state: any) => state.i18n_locales);
23
+ const urlLocale = useGetLocaleFromUrl();
24
+ const defaultLocale = locales.find((locale: any) => locale.isDefault);
25
+ const selectedLocale = form.initialData?.locale ?? urlLocale ?? defaultLocale.code;
26
+ const prevUid = usePrevious(uid);
27
+
28
+ const [selected, setSelected] = useState<SingleValue<IReactSelectValue | null>>(
29
+ getInitialSelectItem(form.initialData?.collectionTypeId, form.initialData?.collectionTypeTitle)
30
+ );
31
+
32
+ const isPagePageType = !uid;
33
+ const searchEntitiesIsEnabled = !isPagePageType;
34
+
35
+ const getItems = async (inputValue?: string): Promise<IReactSelectValue[]> => {
36
+ const searchEntities = await getSearchEntities({
37
+ fetchClient,
38
+ page: PAGE,
39
+ locale: selectedLocale,
40
+ uid,
41
+ searchQuery: inputValue,
42
+ currentCollectionTypeId: form.initialData?.collectionTypeId,
43
+ currentPageId: form.initialData?.id
44
+ });
45
+
46
+ return searchEntities.results.map((x) => ({
47
+ value: String(x.id),
48
+ label: x.title,
49
+ isDisabled: x.hasPages && !x.isCurrentSelected,
50
+ initialSelected: x.isCurrentSelected
51
+ }));
52
+ };
53
+
54
+ const setCollectionTypeId = (item?: SingleValue<IReactSelectValue>) => {
55
+ form.onChange({
56
+ target: {
57
+ name: 'collectionTypeId',
58
+ value: item?.value || null
59
+ },
60
+ shouldSetInitialValue: true
61
+ });
62
+ setSelected(item || null);
63
+ };
64
+
65
+ const handleChange = (item?: SingleValue<IReactSelectValue>) => {
66
+ setCollectionTypeId(item);
67
+ };
68
+
69
+ useEffect(() => {
70
+ if (prevUid !== null && prevUid !== uid) {
71
+ setSelected(null);
72
+ }
73
+ }, [uid]);
74
+
75
+ const debouncedFetch = debounce((searchTerm, callback) => {
76
+ promiseOptions(searchTerm).then((result) => {
77
+ return callback(result || []);
78
+ });
79
+ }, SEARCH_DEBOUNCE_MS);
80
+
81
+ const promiseOptions = (inputValue: string): Promise<IReactSelectValue[]> =>
82
+ new Promise<IReactSelectValue[]>((resolve) => {
83
+ resolve(getItems(inputValue));
84
+ });
85
+
86
+ return (
87
+ <Combobox
88
+ key={`rerenderOnUidChange-${uid}`}
89
+ id="collectionTypeSearch"
90
+ label="Entity"
91
+ loadOptions={debouncedFetch}
92
+ cacheOptions
93
+ onChange={handleChange}
94
+ customOption={CustomOption}
95
+ value={selected}
96
+ defaultOptions={searchEntitiesIsEnabled}
97
+ />
98
+ );
99
+ };
100
+
101
+ const CustomOption = (props: OptionProps<IReactSelectValue, false>) => {
102
+ return (
103
+ <components.Option {...props}>
104
+ <span>{props.children}</span>
105
+ {props.isDisabled && <span>Entity is in use</span>}
106
+ {props.data.initialSelected && <span>Currently selected</span>}
107
+ </components.Option>
108
+ );
109
+ };
110
+
111
+ const getInitialSelectItem = (id?: string, title?: string): SingleValue<IReactSelectValue | null> =>
112
+ id
113
+ ? {
114
+ value: String(id),
115
+ label: title ?? '',
116
+ initialSelected: true
117
+ }
118
+ : null;