@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.
@@ -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;
@@ -0,0 +1,77 @@
1
+ import styled, { css } from 'styled-components';
2
+ import { Box, Link, Typography } from '@strapi/design-system';
3
+
4
+ const EntityLinkWrapper = styled(Typography)`
5
+ ${({ _theme }) => css`
6
+ margin-top: 4px;
7
+ display: flex;
8
+ `}
9
+ `;
10
+
11
+ const EntityLink = styled(Link)`
12
+ ${({ _theme }) => css`
13
+ span {
14
+ line-height: 1.2;
15
+ font-size: inherit;
16
+ display: flex;
17
+ gap: 6px;
18
+ align-items: center;
19
+ }
20
+
21
+ svg {
22
+ width: 0.7rem;
23
+ height: 0.7rem;
24
+ }
25
+ `}
26
+ `;
27
+
28
+ const CustomOption = styled(Box)`
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 8px;
32
+ `;
33
+
34
+ const CustomOptionStatus = styled(Box)`
35
+ background: ${({ theme, publicationState }) =>
36
+ publicationState === 'published' ? theme.colors.success600 : theme.colors.secondary600};
37
+ width: 6px;
38
+ height: 6px;
39
+ border-radius: 100px;
40
+ `;
41
+
42
+ const FieldWrapper = styled(Box)`
43
+ display: flex;
44
+ gap: 4px;
45
+ `;
46
+
47
+ const LinkToPage = styled(Link)`
48
+ ${({ theme, disabled }) => css`
49
+ height: 40px;
50
+ width: 40px;
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ align-self: flex-end;
55
+ flex-shrink: 0;
56
+ border: ${`1px solid ${theme.colors.neutral200}`};
57
+ border-radius: ${theme.borderRadius};
58
+ opacity: ${disabled ? 0.5 : 1};
59
+ > span {
60
+ display: flex;
61
+ }
62
+
63
+ svg {
64
+ width: ${theme.fontSizes[2]};
65
+ height: ${theme.fontSizes[2]};
66
+ }
67
+ `}
68
+ `;
69
+
70
+ export default {
71
+ FieldWrapper,
72
+ EntityLinkWrapper,
73
+ EntityLink,
74
+ CustomOptionStatus,
75
+ CustomOption,
76
+ LinkToPage
77
+ };
@@ -0,0 +1,3 @@
1
+ const getTranslation = (id: string) => `content-manager.${id}`;
2
+
3
+ export { getTranslation };
@@ -0,0 +1,76 @@
1
+ import { IntlFormatters, MessageDescriptor } from '@formatjs/intl';
2
+ import { getObjectFromFormName } from '../../../utils/getObjectFromFormName';
3
+
4
+ export interface GetTargetAttributesResult {
5
+ targetAttributes?: {
6
+ relation: string;
7
+ relationType: string;
8
+ target: string;
9
+ targetModel: string;
10
+ type: string;
11
+ };
12
+ targetFieldValue?: any;
13
+ targetField: string;
14
+ targetFieldName: string;
15
+ componentUid?: string;
16
+ componentId?: number;
17
+ relationMainFieldName?: string;
18
+ }
19
+
20
+ export interface IPlatformFilteredSelectFieldProps {
21
+ intlLabel?: MessageDescriptor & Parameters<IntlFormatters['formatMessage']>;
22
+ onChange?: any;
23
+ attribute?: {
24
+ pluginOptions?: { filteredSelect?: { targetUid?: string; targetField?: string } };
25
+ required?: boolean;
26
+ };
27
+ name?: string;
28
+ description?: MessageDescriptor & Parameters<IntlFormatters['formatMessage']>;
29
+ disabled?: boolean;
30
+ error?: any;
31
+ labelAction?: any;
32
+ required?: boolean;
33
+ value?: string;
34
+ contentTypeUID?: string;
35
+ placeholder?: MessageDescriptor & Parameters<IntlFormatters['formatMessage']>;
36
+ }
37
+
38
+ const RelationHelper = {
39
+ getTargetAttributes: (
40
+ fieldName?: string,
41
+ attribute?: IPlatformFilteredSelectFieldProps['attribute'],
42
+ modifiedData?: Record<string, any>,
43
+ layout?: Record<string, any>,
44
+ allLayoutData?: Record<string, any>
45
+ ) => {
46
+ const targetField = attribute?.pluginOptions?.filteredSelect?.targetField || '';
47
+ const mainObject = getObjectFromFormName(modifiedData, fieldName || '');
48
+ const result: GetTargetAttributesResult = {
49
+ targetAttributes: undefined,
50
+ targetFieldValue: undefined,
51
+ targetField,
52
+ componentUid: '',
53
+ componentId: undefined,
54
+ targetFieldName: targetField,
55
+ relationMainFieldName: ''
56
+ };
57
+ if (mainObject?.__component) {
58
+ const lastFieldName = fieldName?.split('.')?.[fieldName?.split('.')?.length - 1];
59
+ result.targetAttributes = allLayoutData?.components?.[mainObject.__component]?.attributes?.[targetField];
60
+ result.relationMainFieldName =
61
+ allLayoutData?.components?.[mainObject.__component]?.metadatas?.[targetField]?.list?.mainField?.name;
62
+ result.targetFieldValue = mainObject?.[targetField];
63
+ result.targetField = (fieldName || '')?.replace(lastFieldName || '', targetField);
64
+ result.componentUid = mainObject?.__component;
65
+ result.componentId = mainObject?.id;
66
+ } else {
67
+ result.targetAttributes = layout?.attributes?.[targetField];
68
+ result.relationMainFieldName = layout?.metadatas?.[targetField]?.list?.mainField?.name;
69
+ result.targetFieldValue = modifiedData?.[targetField];
70
+ }
71
+
72
+ return result;
73
+ }
74
+ };
75
+
76
+ export default RelationHelper;
@@ -8,6 +8,8 @@ import { EditView } from './components/EditView';
8
8
  import { PageTypeEditView } from './components/PageTypeEditView';
9
9
  import middlewares from './middlewares';
10
10
  import PluginIcon from './components/PluginIcon';
11
+ import getTrad from './utils/getTrad';
12
+ import PlatformFilteredSelectFieldIcon from './components/PlatformFilteredSelectField/InputIcon';
11
13
 
12
14
  const name = pluginPkg.strapi.name;
13
15
 
@@ -21,6 +23,25 @@ export default {
21
23
  name
22
24
  };
23
25
 
26
+ app.customFields.register({
27
+ name: 'filtered-select',
28
+ pluginId,
29
+ type: 'string',
30
+ icon: PlatformFilteredSelectFieldIcon,
31
+ intlLabel: {
32
+ id: getTrad('platformFilteredSelect.builder.label'),
33
+ defaultMessage: name
34
+ },
35
+ intlDescription: {
36
+ id: getTrad('platformFilteredSelect.builder.description'),
37
+ defaultMessage: 'Add a filtered select to a page'
38
+ },
39
+ components: {
40
+ Input: async () =>
41
+ import(/* webpackChunkName: "filtered-select-component" */ './components/PlatformFilteredSelectField')
42
+ }
43
+ });
44
+
24
45
  app.addMenuLink({
25
46
  to: '/plugins/strapi-plugin-page-builder',
26
47
  icon: PluginIcon, // This really is a plugin icon, I promise!
@@ -2,5 +2,8 @@
2
2
  "template.confirmModal.title": "Replace all page modules",
3
3
  "template.confirmModal.body": "You are about to replace all modules on this page. Are you sure you want to continue?",
4
4
  "template.confirmModal.buttons.cancel": "No, cancel",
5
- "template.confirmModal.buttons.submit": "Yes, replace all"
5
+ "template.confirmModal.buttons.submit": "Yes, replace all",
6
+ "platformFilteredSelect.builder.label": "Platform filtered select",
7
+ "platformFilteredSelect.builder.description": "Enables filtering relations by platform",
8
+ "platformFilteredSelect.linkToEntity.label": "Go to page"
6
9
  }
@@ -2,5 +2,8 @@
2
2
  "template.confirmModal.title": "Pagina modules vervangen",
3
3
  "template.confirmModal.body": "Weet je zeker dat je alle modules op deze pagina wilt vervangen?",
4
4
  "template.confirmModal.buttons.cancel": "Nee, annuleer",
5
- "template.confirmModal.buttons.submit": "Ja, vervang modules"
5
+ "template.confirmModal.buttons.submit": "Ja, vervang modules",
6
+ "platformFilteredSelect.builder.label": "Platform filtered select",
7
+ "platformFilteredSelect.builder.description": "Filtert relaties op basis van een platform",
8
+ "platformFilteredSelect.linkToEntity.label": "Ga naar pagina"
6
9
  }
@@ -0,0 +1,31 @@
1
+ import get from 'lodash/get';
2
+
3
+ const getObjectFromFormName = (modifiedData?: Record<string, any>, name?: string) => {
4
+ const split = (name || '')?.split('.');
5
+ let newPath = '';
6
+
7
+ for (let index = 0; index < split.length; index++) {
8
+ let value = split?.[index];
9
+
10
+ if (index + 1 === split.length) {
11
+ break;
12
+ }
13
+
14
+ if (index === 0) {
15
+ newPath += value;
16
+ continue;
17
+ }
18
+
19
+ if (!isNaN(Number(value))) {
20
+ value = `[${value}]`;
21
+ newPath += value;
22
+ continue;
23
+ }
24
+
25
+ newPath += `.${value}`;
26
+ }
27
+
28
+ return get(modifiedData, newPath);
29
+ };
30
+
31
+ export { getObjectFromFormName };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webbio/strapi-plugin-page-builder",
3
- "version": "0.7.0-platform",
3
+ "version": "0.8.0-platform",
4
4
  "description": "This is the description of the plugin.",
5
5
  "scripts": {
6
6
  "develop": "tsc -p tsconfig.server.json -w",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@strapi/strapi": "^4.15.0",
45
- "@webbio/strapi-plugin-slug": "^2.0.5",
45
+ "@webbio/strapi-plugin-slug": "^3.0.0",
46
46
  "react": "^17.0.0 || ^18.0.0",
47
47
  "react-dom": "^17.0.0 || ^18.0.0",
48
48
  "react-router-dom": "^5.3.4",
@@ -61,7 +61,7 @@
61
61
  }
62
62
  ],
63
63
  "engines": {
64
- "node": ">=14.19.1 <=18.x.x",
64
+ "node": ">=14.19.1 <=20.x.x",
65
65
  "npm": ">=6.0.0"
66
66
  },
67
67
  "license": "MIT",
@@ -8,6 +8,11 @@ const page_type_1 = __importDefault(require("./graphql/page-type"));
8
8
  const pages_by_uid_1 = __importDefault(require("./graphql/pages-by-uid"));
9
9
  exports.default = async ({ strapi }) => {
10
10
  var _a, _b;
11
+ strapi.customFields.register({
12
+ name: 'filtered-select',
13
+ plugin: 'page-builder',
14
+ type: 'string'
15
+ });
11
16
  const extensionService = strapi.plugin('graphql').service('extension');
12
17
  extensionService.use(page_type_1.default);
13
18
  extensionService.use((0, page_by_path_1.default)(strapi));