@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.
- package/.prettierignore +17 -0
- package/README.md +3 -0
- package/admin/src/api/collection-type.ts +110 -0
- package/admin/src/api/has-page-relation.ts +34 -0
- package/admin/src/api/page-type.ts +31 -0
- package/admin/src/api/template.ts +25 -0
- package/admin/src/components/Combobox/index.tsx +77 -0
- package/admin/src/components/Combobox/react-select-custom-styles.tsx +111 -0
- package/admin/src/components/Combobox/styles.ts +22 -0
- package/admin/src/components/ConfirmModal/index.tsx +90 -0
- package/admin/src/components/EditView/CollectionTypeSearch/index.tsx +118 -0
- package/admin/src/components/EditView/CollectionTypeSettings/CreatePageButton/index.tsx +95 -0
- package/admin/src/components/EditView/CollectionTypeSettings/CreatePageButton/styles.ts +26 -0
- package/admin/src/components/EditView/CollectionTypeSettings/index.tsx +53 -0
- package/admin/src/components/EditView/Details/index.tsx +47 -0
- package/admin/src/components/EditView/Details/styles.ts +51 -0
- package/admin/src/components/EditView/PageSettings/index.tsx +104 -0
- package/admin/src/components/EditView/Template/TemplateConfirmModal/index.tsx +36 -0
- package/admin/src/components/EditView/Template/TemplateSelect/index.tsx +64 -0
- package/admin/src/components/EditView/Template/TemplateSelect/use-template-modules.ts +30 -0
- package/admin/src/components/EditView/index.tsx +27 -0
- package/admin/src/components/EditView/page-type-select.tsx +30 -0
- package/admin/src/components/EditView/wrapper.tsx +35 -0
- package/admin/src/components/Initializer/index.tsx +24 -0
- package/admin/src/components/PageTypeFilter/index.tsx +17 -0
- package/admin/src/components/PageTypeFilter/page-type-filter.tsx +130 -0
- package/admin/src/components/PluginIcon/index.tsx +12 -0
- package/admin/src/constants.ts +1 -0
- package/admin/src/index.tsx +115 -0
- package/admin/src/pages/App/index.tsx +25 -0
- package/admin/src/pages/HomePage/index.tsx +19 -0
- package/admin/src/pluginId.ts +5 -0
- package/admin/src/redux/initialData.reducer.ts +0 -0
- package/admin/src/translations/en.json +6 -0
- package/admin/src/translations/nl.json +6 -0
- package/admin/src/utils/getRequestUrl.ts +11 -0
- package/admin/src/utils/getTrad.ts +5 -0
- package/admin/src/utils/hooks/useDebounce.ts +17 -0
- package/admin/src/utils/hooks/useGetLocaleFromUrl.ts +9 -0
- package/admin/src/utils/hooks/usePrevious.ts +12 -0
- package/admin/src/utils/sanitizeModules.ts +10 -0
- package/custom.d.ts +5 -0
- package/package.json +71 -0
- package/server/bootstrap.ts +106 -0
- package/server/config/index.ts +4 -0
- package/server/content-types/index.ts +1 -0
- package/server/controllers/collection-types.ts +27 -0
- package/server/controllers/index.ts +9 -0
- package/server/controllers/page-type.ts +12 -0
- package/server/controllers/page.ts +20 -0
- package/server/destroy.ts +5 -0
- package/server/index.ts +23 -0
- package/server/middlewares/index.ts +1 -0
- package/server/policies/index.ts +1 -0
- package/server/register.ts +5 -0
- package/server/routes/index.ts +42 -0
- package/server/schema/page-end.json +97 -0
- package/server/schema/page-start.json +87 -0
- package/server/schema/page-type-end.json +49 -0
- package/server/schema/page-type-start.json +44 -0
- package/server/schema/template.json +35 -0
- package/server/services/builder.ts +121 -0
- package/server/services/collection-types.ts +49 -0
- package/server/services/index.ts +11 -0
- package/server/services/page-type.ts +22 -0
- package/server/services/page.ts +24 -0
- package/server/utils/graphql.ts +110 -0
- package/server/utils/reload-strapi-on-load.ts +13 -0
- package/shared/utils/constants.ts +4 -0
- package/shared/utils/sleep.ts +1 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +3 -0
- package/tsconfig.json +20 -0
- package/tsconfig.server.json +25 -0
- package/tsconfig.tsbuildinfo +1 -0
package/.prettierignore
ADDED
package/README.md
ADDED
|
@@ -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;
|