@enonic/nextjs-adapter 0.0.1-alpha.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/ComponentRegistry.d.ts +61 -0
- package/ComponentRegistry.js +115 -0
- package/README.md +39 -0
- package/UrlProcessor.d.ts +21 -0
- package/UrlProcessor.js +54 -0
- package/baseMappings.d.ts +1 -0
- package/baseMappings.js +38 -0
- package/guillotine/fetchContent.d.ts +82 -0
- package/guillotine/fetchContent.js +644 -0
- package/guillotine/getMetaData.d.ts +91 -0
- package/guillotine/getMetaData.js +128 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +33 -0
- package/utils.d.ts +64 -0
- package/utils.js +146 -0
- package/views/BaseComponent.d.ts +25 -0
- package/views/BaseComponent.jsx +114 -0
- package/views/BaseContent.d.ts +4 -0
- package/views/BaseContent.jsx +41 -0
- package/views/BaseLayout.d.ts +17 -0
- package/views/BaseLayout.jsx +24 -0
- package/views/BaseMacro.d.ts +16 -0
- package/views/BaseMacro.jsx +85 -0
- package/views/BasePage.d.ts +19 -0
- package/views/BasePage.jsx +34 -0
- package/views/BasePart.d.ts +19 -0
- package/views/BasePart.jsx +24 -0
- package/views/Empty.d.ts +3 -0
- package/views/Empty.jsx +3 -0
- package/views/Fragment.d.ts +10 -0
- package/views/Fragment.jsx +25 -0
- package/views/MainView.d.ts +4 -0
- package/views/MainView.jsx +14 -0
- package/views/Region.d.ts +20 -0
- package/views/Region.jsx +57 -0
- package/views/RichTextView.d.ts +15 -0
- package/views/RichTextView.jsx +55 -0
- package/views/SingleComponent.d.ts +5 -0
- package/views/SingleComponent.jsx +11 -0
- package/views/Text.d.ts +9 -0
- package/views/Text.jsx +4 -0
- package/views/macros/DefaultMacro.d.ts +4 -0
- package/views/macros/DefaultMacro.jsx +3 -0
- package/views/macros/DisableMacro.d.ts +3 -0
- package/views/macros/DisableMacro.jsx +2 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RENDER_MODE, XP_COMPONENT_TYPE, XP_REQUEST_TYPE } from '../utils';
|
|
2
|
+
export declare const richTextQuery: (fieldName: string) => string;
|
|
3
|
+
export declare const pageFragmentQuery: () => string;
|
|
4
|
+
export declare function getMetaQuery(pageFragment?: string): string;
|
|
5
|
+
export interface PageComponent {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
type: XP_COMPONENT_TYPE;
|
|
8
|
+
path: string;
|
|
9
|
+
page?: PageData;
|
|
10
|
+
part?: PartData;
|
|
11
|
+
layout?: LayoutData;
|
|
12
|
+
fragment?: FragmentData;
|
|
13
|
+
text?: TextData;
|
|
14
|
+
image?: any;
|
|
15
|
+
regions?: RegionTree;
|
|
16
|
+
data?: any;
|
|
17
|
+
error?: any;
|
|
18
|
+
}
|
|
19
|
+
export interface TextData {
|
|
20
|
+
value: RichTextData;
|
|
21
|
+
}
|
|
22
|
+
export interface RichTextData {
|
|
23
|
+
processedHtml: string;
|
|
24
|
+
links: LinkData[];
|
|
25
|
+
macros: MacroData[];
|
|
26
|
+
images: ImageData[];
|
|
27
|
+
}
|
|
28
|
+
export interface LinkData {
|
|
29
|
+
ref: string;
|
|
30
|
+
media: {
|
|
31
|
+
content: {
|
|
32
|
+
id: string;
|
|
33
|
+
};
|
|
34
|
+
} | null;
|
|
35
|
+
}
|
|
36
|
+
export interface MacroData {
|
|
37
|
+
ref: string;
|
|
38
|
+
name: string;
|
|
39
|
+
descriptor: string;
|
|
40
|
+
config: {
|
|
41
|
+
[name: string]: MacroConfig;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface ImageData {
|
|
45
|
+
ref: string;
|
|
46
|
+
image: {
|
|
47
|
+
id: string;
|
|
48
|
+
} | null;
|
|
49
|
+
}
|
|
50
|
+
export interface MacroConfig {
|
|
51
|
+
[key: string]: any;
|
|
52
|
+
}
|
|
53
|
+
export interface RegionTree {
|
|
54
|
+
[key: string]: PageRegion;
|
|
55
|
+
}
|
|
56
|
+
export interface PageRegion {
|
|
57
|
+
name: string;
|
|
58
|
+
components: PageComponent[];
|
|
59
|
+
}
|
|
60
|
+
export interface PartData {
|
|
61
|
+
descriptor: string;
|
|
62
|
+
config: any;
|
|
63
|
+
[customKeysFromQuery: string]: any;
|
|
64
|
+
}
|
|
65
|
+
export interface LayoutData {
|
|
66
|
+
descriptor: string;
|
|
67
|
+
config?: any;
|
|
68
|
+
[customKeysFromQuery: string]: any;
|
|
69
|
+
}
|
|
70
|
+
export interface PageData {
|
|
71
|
+
descriptor: string;
|
|
72
|
+
config?: any;
|
|
73
|
+
template?: string | null;
|
|
74
|
+
regions?: RegionTree;
|
|
75
|
+
}
|
|
76
|
+
export interface FragmentData {
|
|
77
|
+
fragment: {
|
|
78
|
+
components: PageComponent[];
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export interface MetaData {
|
|
82
|
+
type: string;
|
|
83
|
+
path: string;
|
|
84
|
+
requestType: XP_REQUEST_TYPE;
|
|
85
|
+
renderMode: RENDER_MODE;
|
|
86
|
+
requestedComponent?: PageComponent;
|
|
87
|
+
canRender: boolean;
|
|
88
|
+
catchAll: boolean;
|
|
89
|
+
apiUrl: string;
|
|
90
|
+
baseUrl: string;
|
|
91
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { sanitizeGraphqlName } from '../utils';
|
|
2
|
+
import { ComponentRegistry } from '../ComponentRegistry';
|
|
3
|
+
const macroConfigQuery = () => {
|
|
4
|
+
return configQuery(ComponentRegistry.getMacros(), false, false);
|
|
5
|
+
};
|
|
6
|
+
const partConfigQuery = () => {
|
|
7
|
+
return configQuery(ComponentRegistry.getParts());
|
|
8
|
+
};
|
|
9
|
+
const layoutConfigQuery = () => {
|
|
10
|
+
return configQuery(ComponentRegistry.getLayouts());
|
|
11
|
+
};
|
|
12
|
+
const pageConfigQuery = () => {
|
|
13
|
+
return configQuery(ComponentRegistry.getPages());
|
|
14
|
+
};
|
|
15
|
+
const configQuery = (list, includeAppName = true, canUseConfigAsJson = true) => {
|
|
16
|
+
const hasQueryList = (list === null || list === void 0 ? void 0 : list.filter(([key, def]) => def.configQuery)) || [];
|
|
17
|
+
if (hasQueryList.length === 0) {
|
|
18
|
+
return canUseConfigAsJson ? 'configAsJson' : '';
|
|
19
|
+
}
|
|
20
|
+
const configsByApp = {};
|
|
21
|
+
hasQueryList
|
|
22
|
+
.forEach((entry) => {
|
|
23
|
+
const nameParts = entry[0].split(':');
|
|
24
|
+
if (nameParts.length === 2) {
|
|
25
|
+
const sanitizedAppName = sanitizeGraphqlName(nameParts[0]);
|
|
26
|
+
let existingConfigs = configsByApp[sanitizedAppName];
|
|
27
|
+
if (!existingConfigs) {
|
|
28
|
+
existingConfigs = [];
|
|
29
|
+
configsByApp[sanitizedAppName] = existingConfigs;
|
|
30
|
+
}
|
|
31
|
+
existingConfigs.push(`${sanitizeGraphqlName(nameParts[1])}${entry[1].configQuery}`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// Still query for configAsJson if at least one item has no configQuery defined
|
|
35
|
+
const configAsJsonQuery = canUseConfigAsJson && hasQueryList.length < list.length ? 'configAsJson' : '';
|
|
36
|
+
if (!includeAppName) {
|
|
37
|
+
return `${configAsJsonQuery}
|
|
38
|
+
config {
|
|
39
|
+
${Object.values(configsByApp).reduce((arr, curr) => arr.concat(curr), []).join('\n')}
|
|
40
|
+
}`;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
return `${configAsJsonQuery}
|
|
44
|
+
config {
|
|
45
|
+
${Object.entries(configsByApp).map(entry => entry[0] + '{\n' + entry[1].join('\n') + '\n}')}
|
|
46
|
+
}`;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/*
|
|
50
|
+
IMPORTANT: make sure to transform your queries into functions too when using this function inside them !!!
|
|
51
|
+
*/
|
|
52
|
+
export const richTextQuery = (fieldName) => {
|
|
53
|
+
return `${fieldName}(processHtml:{type:absolute, imageWidths:[400, 800, 1200], imageSizes:"(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"}) {
|
|
54
|
+
processedHtml
|
|
55
|
+
macros {
|
|
56
|
+
ref
|
|
57
|
+
name
|
|
58
|
+
descriptor
|
|
59
|
+
${macroConfigQuery()}
|
|
60
|
+
}
|
|
61
|
+
links {
|
|
62
|
+
ref
|
|
63
|
+
media {
|
|
64
|
+
content {
|
|
65
|
+
_id
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
images {
|
|
70
|
+
ref
|
|
71
|
+
image {
|
|
72
|
+
_id
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}`;
|
|
76
|
+
};
|
|
77
|
+
const componentsQuery = () => `
|
|
78
|
+
type
|
|
79
|
+
path
|
|
80
|
+
page {
|
|
81
|
+
descriptor
|
|
82
|
+
${pageConfigQuery()}
|
|
83
|
+
template {
|
|
84
|
+
_path
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
layout {
|
|
88
|
+
descriptor
|
|
89
|
+
${layoutConfigQuery()}
|
|
90
|
+
}
|
|
91
|
+
text {
|
|
92
|
+
${richTextQuery('value')}
|
|
93
|
+
}
|
|
94
|
+
part {
|
|
95
|
+
descriptor
|
|
96
|
+
${partConfigQuery()}
|
|
97
|
+
}
|
|
98
|
+
image {
|
|
99
|
+
caption
|
|
100
|
+
image {
|
|
101
|
+
imageUrl (type:absolute, scale: "width-768")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
// THIS QUERY DOES NOT SUPPORT NESTED FRAGMENTS
|
|
106
|
+
export const pageFragmentQuery = () => `
|
|
107
|
+
components(resolveFragment: false, resolveTemplate: true) {
|
|
108
|
+
fragment {
|
|
109
|
+
id
|
|
110
|
+
fragment {
|
|
111
|
+
components {
|
|
112
|
+
${componentsQuery()}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
${componentsQuery()}
|
|
117
|
+
}`;
|
|
118
|
+
export function getMetaQuery(pageFragment) {
|
|
119
|
+
return `query($path:ID!){
|
|
120
|
+
guillotine {
|
|
121
|
+
get(key:$path) {
|
|
122
|
+
_path
|
|
123
|
+
type
|
|
124
|
+
${pageFragment || ''}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}`;
|
|
128
|
+
}
|
package/index.d.ts
ADDED
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@enonic/nextjs-adapter",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"copy": "npx --yes cpy-cli package.json README.md dist",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"lint": "eslint --quiet .",
|
|
13
|
+
"lint:fix": "eslint --fix ."
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"html-react-parser": "^3.0.4",
|
|
17
|
+
"next": "^13.0.2",
|
|
18
|
+
"react": "^18.2.0",
|
|
19
|
+
"react-dom": "^18.2.0",
|
|
20
|
+
"unescape": "^1.0.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@enonic/eslint-config": "^0.1.0",
|
|
24
|
+
"@types/node": "^18.11.9",
|
|
25
|
+
"@types/react": "^18.0.25",
|
|
26
|
+
"@types/react-dom": "^18.0.8",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
|
28
|
+
"@typescript-eslint/parser": "^5.42.1",
|
|
29
|
+
"eslint": "^8.27.0",
|
|
30
|
+
"eslint-plugin-react": "^7.31.10",
|
|
31
|
+
"typescript": "^4.8.4"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/utils.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Import config values from .env, .env.development and .env.production */
|
|
2
|
+
import { Context, MinimalContext } from './guillotine/fetchContent';
|
|
3
|
+
export declare const IS_DEV_MODE: boolean;
|
|
4
|
+
/** Optional utility value - defining in one place the name of the target app (the app that defines the content types, the app name is therefore part of the content type strings used both in typeselector and in query introspections) */
|
|
5
|
+
export declare const APP_NAME: string;
|
|
6
|
+
/** Optional utility value - derived from APP_NAME, only with underscores instead of dots */
|
|
7
|
+
export declare const APP_NAME_UNDERSCORED: string;
|
|
8
|
+
/** Optional utility value - derived from APP_NAME, only with dashes instead of dots */
|
|
9
|
+
export declare const APP_NAME_DASHED: string;
|
|
10
|
+
export declare const SITE_KEY: string;
|
|
11
|
+
export declare const FROM_XP_PARAM = "__fromxp__";
|
|
12
|
+
export declare const XP_BASE_URL_HEADER = "xpbaseurl";
|
|
13
|
+
export declare const COMPONENT_SUBPATH_HEADER = "xp-component-path";
|
|
14
|
+
export declare const RENDER_MODE_HEADER = "content-studio-mode";
|
|
15
|
+
export declare const PORTAL_COMPONENT_ATTRIBUTE = "data-portal-component-type";
|
|
16
|
+
export declare const PORTAL_REGION_ATTRIBUTE = "data-portal-region";
|
|
17
|
+
export declare const FRAGMENT_CONTENTTYPE_NAME = "portal:fragment";
|
|
18
|
+
export declare const FRAGMENT_DEFAULT_REGION_NAME = "fragment";
|
|
19
|
+
export declare const PAGE_TEMPLATE_CONTENTTYPE_NAME = "portal:page-template";
|
|
20
|
+
export declare const PAGE_TEMPLATE_FOLDER = "portal:template-folder";
|
|
21
|
+
export declare const SITE_CONTENTTYPE_NAME = "portal:site";
|
|
22
|
+
export declare enum XP_REQUEST_TYPE {
|
|
23
|
+
COMPONENT = "component",
|
|
24
|
+
TYPE = "type",
|
|
25
|
+
PAGE = "page"
|
|
26
|
+
}
|
|
27
|
+
export declare enum RENDER_MODE {
|
|
28
|
+
INLINE = "inline",
|
|
29
|
+
EDIT = "edit",
|
|
30
|
+
PREVIEW = "preview",
|
|
31
|
+
LIVE = "live",
|
|
32
|
+
ADMIN = "admin",
|
|
33
|
+
NEXT = "next"
|
|
34
|
+
}
|
|
35
|
+
export declare enum XP_COMPONENT_TYPE {
|
|
36
|
+
PART = "part",
|
|
37
|
+
LAYOUT = "layout",
|
|
38
|
+
TEXT = "text",
|
|
39
|
+
FRAGMENT = "fragment",
|
|
40
|
+
PAGE = "page"
|
|
41
|
+
}
|
|
42
|
+
/** Returns true if the context object (from next.js in [[...contentPath]].jsx ) stems from a request that comes from XP in a CS-preview, i.e. has the URI param FROM_XP_PARAM (defined as '__fromXp__' above).
|
|
43
|
+
* False if no context, query or FROM_XP_PARAM param */
|
|
44
|
+
export declare const isRequestFromXP: (context?: Context) => boolean;
|
|
45
|
+
export declare const getXPRequestType: (context?: Context) => XP_REQUEST_TYPE;
|
|
46
|
+
export declare function getContentApiUrl(context?: MinimalContext): string;
|
|
47
|
+
export declare const getXpBaseUrl: (context?: MinimalContext) => string;
|
|
48
|
+
export declare const commonChars: (s1?: string, s2?: string) => string;
|
|
49
|
+
export declare const sanitizeGraphqlName: (text: string) => string;
|
|
50
|
+
declare const adapterConstants: {
|
|
51
|
+
IS_DEV_MODE: boolean;
|
|
52
|
+
APP_NAME: string;
|
|
53
|
+
APP_NAME_UNDERSCORED: string;
|
|
54
|
+
APP_NAME_DASHED: string;
|
|
55
|
+
SITE_KEY: string;
|
|
56
|
+
FROM_XP_PARAM: string;
|
|
57
|
+
COMPONENT_SUBPATH_HEADER: string;
|
|
58
|
+
PORTAL_COMPONENT_ATTRIBUTE: string;
|
|
59
|
+
PORTAL_REGION_ATTRIBUTE: string;
|
|
60
|
+
getXPRequestType: (context?: Context) => XP_REQUEST_TYPE;
|
|
61
|
+
getSingleComponentPath: (context?: Context) => string | undefined;
|
|
62
|
+
getRenderMode: (context?: MinimalContext) => RENDER_MODE;
|
|
63
|
+
};
|
|
64
|
+
export default adapterConstants;
|
package/utils.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const mode = process.env.MODE || process.env.NEXT_PUBLIC_MODE;
|
|
2
|
+
export const IS_DEV_MODE = (mode === 'development');
|
|
3
|
+
/** URL to the guillotine API */
|
|
4
|
+
const CONTENT_API_DRAFT = (process.env.CONTENT_API_DRAFT || process.env.NEXT_PUBLIC_CONTENT_API_DRAFT);
|
|
5
|
+
const CONTENT_API_MASTER = (process.env.CONTENT_API_MASTER || process.env.NEXT_PUBLIC_CONTENT_API_MASTER);
|
|
6
|
+
/** Optional utility value - defining in one place the name of the target app (the app that defines the content types, the app name is therefore part of the content type strings used both in typeselector and in query introspections) */
|
|
7
|
+
export const APP_NAME = (process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME);
|
|
8
|
+
/** Optional utility value - derived from APP_NAME, only with underscores instead of dots */
|
|
9
|
+
export const APP_NAME_UNDERSCORED = (APP_NAME || '').replace(/\./g, '_');
|
|
10
|
+
/** Optional utility value - derived from APP_NAME, only with dashes instead of dots */
|
|
11
|
+
export const APP_NAME_DASHED = (APP_NAME || '').replace(/\./g, '-');
|
|
12
|
+
export const SITE_KEY = (process.env.SITE_KEY || process.env.NEXT_PUBLIC_SITE_KEY);
|
|
13
|
+
// //////////////////////////////////////////////////////////////////////// Hardcode-able constants
|
|
14
|
+
// URI parameter marking that a request is for a preview for CS. MUST MATCH THE VALUE OF 'FROM_XP_PARAM' on XP side.
|
|
15
|
+
export const FROM_XP_PARAM = '__fromxp__';
|
|
16
|
+
export const XP_BASE_URL_HEADER = 'xpbaseurl';
|
|
17
|
+
export const COMPONENT_SUBPATH_HEADER = 'xp-component-path';
|
|
18
|
+
export const RENDER_MODE_HEADER = 'content-studio-mode';
|
|
19
|
+
export const PORTAL_COMPONENT_ATTRIBUTE = 'data-portal-component-type';
|
|
20
|
+
export const PORTAL_REGION_ATTRIBUTE = 'data-portal-region';
|
|
21
|
+
export const FRAGMENT_CONTENTTYPE_NAME = 'portal:fragment';
|
|
22
|
+
export const FRAGMENT_DEFAULT_REGION_NAME = 'fragment';
|
|
23
|
+
export const PAGE_TEMPLATE_CONTENTTYPE_NAME = 'portal:page-template';
|
|
24
|
+
export const PAGE_TEMPLATE_FOLDER = 'portal:template-folder';
|
|
25
|
+
export const SITE_CONTENTTYPE_NAME = 'portal:site';
|
|
26
|
+
// ------------------------------- Exports and auxillary functions derived from values above ------------------------------------
|
|
27
|
+
export var XP_REQUEST_TYPE;
|
|
28
|
+
(function (XP_REQUEST_TYPE) {
|
|
29
|
+
XP_REQUEST_TYPE["COMPONENT"] = "component";
|
|
30
|
+
XP_REQUEST_TYPE["TYPE"] = "type";
|
|
31
|
+
XP_REQUEST_TYPE["PAGE"] = "page";
|
|
32
|
+
})(XP_REQUEST_TYPE || (XP_REQUEST_TYPE = {}));
|
|
33
|
+
export var RENDER_MODE;
|
|
34
|
+
(function (RENDER_MODE) {
|
|
35
|
+
RENDER_MODE["INLINE"] = "inline";
|
|
36
|
+
RENDER_MODE["EDIT"] = "edit";
|
|
37
|
+
RENDER_MODE["PREVIEW"] = "preview";
|
|
38
|
+
RENDER_MODE["LIVE"] = "live";
|
|
39
|
+
RENDER_MODE["ADMIN"] = "admin";
|
|
40
|
+
RENDER_MODE["NEXT"] = "next"; // Fallback: not using XP proxy but rendering directly with next.js
|
|
41
|
+
})(RENDER_MODE || (RENDER_MODE = {}));
|
|
42
|
+
// TODO: Use these instead of hardcoded strings everywhere
|
|
43
|
+
export var XP_COMPONENT_TYPE;
|
|
44
|
+
(function (XP_COMPONENT_TYPE) {
|
|
45
|
+
XP_COMPONENT_TYPE["PART"] = "part";
|
|
46
|
+
XP_COMPONENT_TYPE["LAYOUT"] = "layout";
|
|
47
|
+
XP_COMPONENT_TYPE["TEXT"] = "text";
|
|
48
|
+
XP_COMPONENT_TYPE["FRAGMENT"] = "fragment";
|
|
49
|
+
XP_COMPONENT_TYPE["PAGE"] = "page";
|
|
50
|
+
})(XP_COMPONENT_TYPE || (XP_COMPONENT_TYPE = {}));
|
|
51
|
+
/** Returns true if the context object (from next.js in [[...contentPath]].jsx ) stems from a request that comes from XP in a CS-preview, i.e. has the URI param FROM_XP_PARAM (defined as '__fromXp__' above).
|
|
52
|
+
* False if no context, query or FROM_XP_PARAM param */
|
|
53
|
+
export const isRequestFromXP = (context) => {
|
|
54
|
+
return !!getXPRequestType(context);
|
|
55
|
+
};
|
|
56
|
+
export const getXPRequestType = (context) => {
|
|
57
|
+
var _a;
|
|
58
|
+
const headerValue = (((_a = context === null || context === void 0 ? void 0 : context.req) === null || _a === void 0 ? void 0 : _a.headers) || {})[FROM_XP_PARAM];
|
|
59
|
+
const enumValue = XP_REQUEST_TYPE[headerValue === null || headerValue === void 0 ? void 0 : headerValue.toUpperCase()];
|
|
60
|
+
return enumValue || XP_REQUEST_TYPE.PAGE; // need to have some defaults here in case of rendering without XP
|
|
61
|
+
};
|
|
62
|
+
const getRenderMode = (context) => {
|
|
63
|
+
var _a;
|
|
64
|
+
const value = (((_a = context === null || context === void 0 ? void 0 : context.req) === null || _a === void 0 ? void 0 : _a.headers) || {})[RENDER_MODE_HEADER];
|
|
65
|
+
const enumValue = RENDER_MODE[value === null || value === void 0 ? void 0 : value.toUpperCase()];
|
|
66
|
+
return enumValue || RENDER_MODE[process.env.RENDER_MODE] || RENDER_MODE.NEXT;
|
|
67
|
+
};
|
|
68
|
+
const getSingleComponentPath = (context) => {
|
|
69
|
+
var _a;
|
|
70
|
+
return (((_a = context === null || context === void 0 ? void 0 : context.req) === null || _a === void 0 ? void 0 : _a.headers) || {})[COMPONENT_SUBPATH_HEADER];
|
|
71
|
+
};
|
|
72
|
+
export function getContentApiUrl(context) {
|
|
73
|
+
const mode = getRenderMode(context);
|
|
74
|
+
return mode === RENDER_MODE.NEXT ? CONTENT_API_MASTER : CONTENT_API_DRAFT;
|
|
75
|
+
}
|
|
76
|
+
/** For '<a href="..."' link values in props when clicking the link should navigate to an XP content item page
|
|
77
|
+
* and the query returns the XP _path to the target content item:
|
|
78
|
+
* When viewed directly, the header will have a `<base href='/' />` (see src/pages/_app.tsx), and when viewed through an
|
|
79
|
+
* XP Content Studio preview, lib-nextjs-proxy will add `<base href='xp/relevant/root/site/url/' />`.
|
|
80
|
+
* So for content-item links to work in BOTH contexts, the href value should be the path relative to the root site item, not starting with a slash.
|
|
81
|
+
* */
|
|
82
|
+
// export const getContentLinkUrlFromXpPath = (_path: string): string => _path.replace(siteNamePattern, '')
|
|
83
|
+
const xpBaseUrlMap = {};
|
|
84
|
+
export const getXpBaseUrl = (context) => {
|
|
85
|
+
var _a;
|
|
86
|
+
const mode = getRenderMode(context);
|
|
87
|
+
const existingUrl = xpBaseUrlMap[mode];
|
|
88
|
+
if (existingUrl) {
|
|
89
|
+
return existingUrl;
|
|
90
|
+
}
|
|
91
|
+
const header = ((((_a = context === null || context === void 0 ? void 0 : context.req) === null || _a === void 0 ? void 0 : _a.headers) || {})[XP_BASE_URL_HEADER] || '');
|
|
92
|
+
// TODO: workaround for XP pattern controller mapping not picked up in edit mode
|
|
93
|
+
const resultingUrl = (header || '/').replace(/\/edit\//, '/inline/');
|
|
94
|
+
xpBaseUrlMap[mode] = resultingUrl;
|
|
95
|
+
return resultingUrl;
|
|
96
|
+
};
|
|
97
|
+
export const commonChars = (s1, s2) => {
|
|
98
|
+
let result = '';
|
|
99
|
+
if (!s1 || s1.length === 0 || !s2 || s2.length === 0) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
for (let i = 0; i < s1.length; i++) {
|
|
103
|
+
const s1Element = s1[i];
|
|
104
|
+
if (s2[i] === s1Element) {
|
|
105
|
+
result += s1Element;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
};
|
|
113
|
+
// sanitizes text according to graphql naming spec http://spec.graphql.org/October2021/#sec-Names
|
|
114
|
+
export const sanitizeGraphqlName = (text) => {
|
|
115
|
+
if (!text || text.trim().length === 0) {
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
let result = text.replace(/([^0-9A-Za-z])+/g, '_');
|
|
119
|
+
if (result.length > 0 && /[0-9]/.test(result.charAt(0))) {
|
|
120
|
+
result = '_' + result;
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
};
|
|
124
|
+
// ---------------------------------------------------------------------------------------------------------------- Export
|
|
125
|
+
const adapterConstants = {
|
|
126
|
+
IS_DEV_MODE,
|
|
127
|
+
APP_NAME,
|
|
128
|
+
APP_NAME_UNDERSCORED,
|
|
129
|
+
APP_NAME_DASHED,
|
|
130
|
+
SITE_KEY,
|
|
131
|
+
FROM_XP_PARAM,
|
|
132
|
+
COMPONENT_SUBPATH_HEADER,
|
|
133
|
+
PORTAL_COMPONENT_ATTRIBUTE,
|
|
134
|
+
PORTAL_REGION_ATTRIBUTE,
|
|
135
|
+
getXPRequestType,
|
|
136
|
+
getSingleComponentPath,
|
|
137
|
+
getRenderMode,
|
|
138
|
+
};
|
|
139
|
+
// Verify required values
|
|
140
|
+
const NOT_REQUIRED = ['IS_DEV_MODE'];
|
|
141
|
+
Object.keys(adapterConstants).forEach(key => {
|
|
142
|
+
if (NOT_REQUIRED.indexOf(key) === -1 && !adapterConstants[key]) {
|
|
143
|
+
throw Error(`constants.ts: Config value '${key}' is missing (from .env?)`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
export default adapterConstants;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { MetaData, PageComponent } from '../guillotine/getMetaData';
|
|
3
|
+
export declare type BaseComponentProps = {
|
|
4
|
+
component: PageComponent;
|
|
5
|
+
meta: MetaData;
|
|
6
|
+
common?: any;
|
|
7
|
+
};
|
|
8
|
+
declare const BaseComponent: ({ component, meta, common }: BaseComponentProps) => JSX.Element;
|
|
9
|
+
export default BaseComponent;
|
|
10
|
+
export declare const MissingComponent: ({ descriptor, type }: {
|
|
11
|
+
descriptor?: string;
|
|
12
|
+
type: string;
|
|
13
|
+
}) => JSX.Element;
|
|
14
|
+
export declare function shouldShowMissingView(meta: MetaData): boolean;
|
|
15
|
+
export declare const ErrorComponent: ({ type, descriptor, reason }: {
|
|
16
|
+
type?: string;
|
|
17
|
+
descriptor?: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}) => JSX.Element;
|
|
20
|
+
export declare function shouldShowErrorView(meta: MetaData): boolean;
|
|
21
|
+
export declare const PlaceholderComponent: ({ type, descriptor }: {
|
|
22
|
+
type?: string;
|
|
23
|
+
descriptor?: string;
|
|
24
|
+
}) => JSX.Element;
|
|
25
|
+
export declare function shouldShowPlaceholderView(meta: MetaData): boolean;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IS_DEV_MODE, PORTAL_COMPONENT_ATTRIBUTE, RENDER_MODE, XP_COMPONENT_TYPE } from '../utils';
|
|
3
|
+
import { ComponentRegistry } from '../ComponentRegistry';
|
|
4
|
+
import Empty from './Empty';
|
|
5
|
+
import * as ReactDOMServer from 'react-dom/server';
|
|
6
|
+
const BaseComponent = ({ component, meta, common }) => {
|
|
7
|
+
var _a;
|
|
8
|
+
const { type, error } = component;
|
|
9
|
+
const cmpData = component[type];
|
|
10
|
+
const descriptor = cmpData && 'descriptor' in cmpData ? cmpData.descriptor : undefined;
|
|
11
|
+
let ComponentView;
|
|
12
|
+
if (error) {
|
|
13
|
+
// renders error component when rendering whole page
|
|
14
|
+
console.warn(`BaseComponent: ${type} '${descriptor}' error: ${error}`);
|
|
15
|
+
ComponentView = shouldShowErrorView(meta) ? <ErrorComponent type={type} reason={error} descriptor={descriptor}/> : <Empty />;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const ViewFn = (_a = ComponentRegistry.getComponent(type)) === null || _a === void 0 ? void 0 : _a.view;
|
|
19
|
+
if (!ViewFn) {
|
|
20
|
+
// show missing component kind, i.e. 'part', 'layout', 'text'
|
|
21
|
+
console.warn(`BaseComponent: can not render component '${type}': no next view or catch-all defined`);
|
|
22
|
+
ComponentView = shouldShowMissingView(meta) ? <MissingComponent type={type} descriptor={descriptor}/> : <Empty />;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const cmpAttrs = createComponentAttrs(component, meta, common);
|
|
26
|
+
ComponentView = <ViewFn {...cmpAttrs}/>;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// need to display a placeholder if descriptor is empty as component is not initialized yet
|
|
30
|
+
if (descriptor && shouldShowPlaceholderView(meta)) {
|
|
31
|
+
const outputHTML = ReactDOMServer.renderToString(ComponentView);
|
|
32
|
+
if (!outputHTML || outputHTML.trim().length === 0) {
|
|
33
|
+
// render some placeholder in case of empty output
|
|
34
|
+
ComponentView = <PlaceholderComponent type={type} descriptor={descriptor}/>;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (meta.renderMode === RENDER_MODE.EDIT) {
|
|
38
|
+
return (<div {...createEditorAttrs(type)}>
|
|
39
|
+
{ComponentView}
|
|
40
|
+
</div>);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// do not make component wrappers in live mode
|
|
44
|
+
return ComponentView;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export default BaseComponent;
|
|
48
|
+
/*
|
|
49
|
+
* Used for displaying components with missing implementations on the page, while rendering whole page
|
|
50
|
+
*/
|
|
51
|
+
export const MissingComponent = ({ descriptor, type }) => {
|
|
52
|
+
return (<div style={{
|
|
53
|
+
border: '2px dashed lightgrey',
|
|
54
|
+
padding: '16px',
|
|
55
|
+
}}>
|
|
56
|
+
<h3 style={{ margin: 0 }}>Missing component</h3>
|
|
57
|
+
<p style={{ marginBottom: 0, color: 'grey' }}>{`Missing ${type} with descriptor: ${descriptor}`}</p>
|
|
58
|
+
</div>);
|
|
59
|
+
};
|
|
60
|
+
export function shouldShowMissingView(meta) {
|
|
61
|
+
return IS_DEV_MODE || meta.renderMode !== RENDER_MODE.NEXT;
|
|
62
|
+
}
|
|
63
|
+
/*
|
|
64
|
+
* Used for displaying components with errors on the page, while rendering whole page
|
|
65
|
+
*/
|
|
66
|
+
export const ErrorComponent = ({ type, descriptor, reason }) => {
|
|
67
|
+
return (<div style={{
|
|
68
|
+
border: '2px solid red',
|
|
69
|
+
padding: '16px',
|
|
70
|
+
}}>
|
|
71
|
+
<h3 style={{ margin: 0, textTransform: 'capitalize' }}>{reason ? reason : 'Unknown error'}</h3>
|
|
72
|
+
{descriptor && <p style={{ color: 'grey' }}>{`${type}: ${descriptor}`}</p>}
|
|
73
|
+
</div>);
|
|
74
|
+
};
|
|
75
|
+
export function shouldShowErrorView(meta) {
|
|
76
|
+
return meta.renderMode === RENDER_MODE.EDIT;
|
|
77
|
+
}
|
|
78
|
+
function createEditorAttrs(type) {
|
|
79
|
+
return {
|
|
80
|
+
[PORTAL_COMPONENT_ATTRIBUTE]: type,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createComponentAttrs(component, meta, common) {
|
|
84
|
+
const { type, data, error } = component;
|
|
85
|
+
const cmpAttrs = {
|
|
86
|
+
component: component[type],
|
|
87
|
+
path: component.path,
|
|
88
|
+
meta,
|
|
89
|
+
common,
|
|
90
|
+
};
|
|
91
|
+
if (data) {
|
|
92
|
+
cmpAttrs.data = data;
|
|
93
|
+
}
|
|
94
|
+
if (error) {
|
|
95
|
+
cmpAttrs.error = error;
|
|
96
|
+
}
|
|
97
|
+
if (component.type === XP_COMPONENT_TYPE.LAYOUT) {
|
|
98
|
+
// add regions to layout because they are not present in component[component.type] above
|
|
99
|
+
cmpAttrs.regions = component.regions;
|
|
100
|
+
}
|
|
101
|
+
return cmpAttrs;
|
|
102
|
+
}
|
|
103
|
+
export const PlaceholderComponent = ({ type, descriptor }) => {
|
|
104
|
+
return (<div style={{
|
|
105
|
+
border: '2px solid lightgrey',
|
|
106
|
+
padding: '16px',
|
|
107
|
+
}}>
|
|
108
|
+
<h3 style={{ margin: 0, textTransform: 'capitalize' }}>Empty output</h3>
|
|
109
|
+
{descriptor && <p style={{ color: 'grey' }}>{`${type} '${descriptor}' output is empty`}</p>}
|
|
110
|
+
</div>);
|
|
111
|
+
};
|
|
112
|
+
export function shouldShowPlaceholderView(meta) {
|
|
113
|
+
return meta.renderMode === RENDER_MODE.EDIT;
|
|
114
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ComponentRegistry } from '../ComponentRegistry';
|
|
3
|
+
import BasePage from './BasePage';
|
|
4
|
+
const BaseContent = (props) => {
|
|
5
|
+
const { common, meta, page } = props;
|
|
6
|
+
if (!(meta === null || meta === void 0 ? void 0 : meta.type)) {
|
|
7
|
+
console.warn("BaseContent props are missing 'meta.type'");
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const pageData = page === null || page === void 0 ? void 0 : page.page;
|
|
11
|
+
const pageDesc = pageData === null || pageData === void 0 ? void 0 : pageData.descriptor;
|
|
12
|
+
const typeDef = ComponentRegistry.getContentType(meta.type);
|
|
13
|
+
const ContentTypeView = typeDef === null || typeDef === void 0 ? void 0 : typeDef.view;
|
|
14
|
+
if (ContentTypeView && !(typeDef === null || typeDef === void 0 ? void 0 : typeDef.catchAll)) {
|
|
15
|
+
// console.info(`BaseContent: rendering '${meta.type}' with content type: ${ContentTypeView.name}`);
|
|
16
|
+
return <ContentTypeView {...props}/>;
|
|
17
|
+
}
|
|
18
|
+
else if (pageDesc) {
|
|
19
|
+
// console.info(`BaseContent: rendering '${meta.type}' with page: ${BasePage.name}`);
|
|
20
|
+
const pageAttrs = {
|
|
21
|
+
component: pageData,
|
|
22
|
+
path: page === null || page === void 0 ? void 0 : page.path,
|
|
23
|
+
meta,
|
|
24
|
+
common,
|
|
25
|
+
};
|
|
26
|
+
if (page === null || page === void 0 ? void 0 : page.data) {
|
|
27
|
+
pageAttrs.data = page.data;
|
|
28
|
+
}
|
|
29
|
+
if (page === null || page === void 0 ? void 0 : page.error) {
|
|
30
|
+
pageAttrs.error = page.error;
|
|
31
|
+
}
|
|
32
|
+
return <BasePage {...pageAttrs}/>;
|
|
33
|
+
}
|
|
34
|
+
else if (ContentTypeView) {
|
|
35
|
+
// console.info(`BaseContent: rendering '${meta.type}' with content type catch-all: ${ContentTypeView.name}`);
|
|
36
|
+
return <ContentTypeView {...props}/>;
|
|
37
|
+
}
|
|
38
|
+
console.log(`BaseContent: can not render ${meta.type}: no content type or page view defined`);
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
export default BaseContent;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { LayoutData, MetaData, RegionTree } from '../guillotine/getMetaData';
|
|
3
|
+
export interface LayoutProps {
|
|
4
|
+
layout: LayoutData;
|
|
5
|
+
path: string;
|
|
6
|
+
common: any;
|
|
7
|
+
meta: MetaData;
|
|
8
|
+
}
|
|
9
|
+
interface BaseLayoutProps {
|
|
10
|
+
component?: LayoutData;
|
|
11
|
+
path: string;
|
|
12
|
+
regions?: RegionTree;
|
|
13
|
+
common?: any;
|
|
14
|
+
meta: MetaData;
|
|
15
|
+
}
|
|
16
|
+
declare const BaseLayout: (props: BaseLayoutProps) => JSX.Element;
|
|
17
|
+
export default BaseLayout;
|