@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.
Files changed (46) hide show
  1. package/ComponentRegistry.d.ts +61 -0
  2. package/ComponentRegistry.js +115 -0
  3. package/README.md +39 -0
  4. package/UrlProcessor.d.ts +21 -0
  5. package/UrlProcessor.js +54 -0
  6. package/baseMappings.d.ts +1 -0
  7. package/baseMappings.js +38 -0
  8. package/guillotine/fetchContent.d.ts +82 -0
  9. package/guillotine/fetchContent.js +644 -0
  10. package/guillotine/getMetaData.d.ts +91 -0
  11. package/guillotine/getMetaData.js +128 -0
  12. package/index.d.ts +2 -0
  13. package/index.js +2 -0
  14. package/package.json +33 -0
  15. package/utils.d.ts +64 -0
  16. package/utils.js +146 -0
  17. package/views/BaseComponent.d.ts +25 -0
  18. package/views/BaseComponent.jsx +114 -0
  19. package/views/BaseContent.d.ts +4 -0
  20. package/views/BaseContent.jsx +41 -0
  21. package/views/BaseLayout.d.ts +17 -0
  22. package/views/BaseLayout.jsx +24 -0
  23. package/views/BaseMacro.d.ts +16 -0
  24. package/views/BaseMacro.jsx +85 -0
  25. package/views/BasePage.d.ts +19 -0
  26. package/views/BasePage.jsx +34 -0
  27. package/views/BasePart.d.ts +19 -0
  28. package/views/BasePart.jsx +24 -0
  29. package/views/Empty.d.ts +3 -0
  30. package/views/Empty.jsx +3 -0
  31. package/views/Fragment.d.ts +10 -0
  32. package/views/Fragment.jsx +25 -0
  33. package/views/MainView.d.ts +4 -0
  34. package/views/MainView.jsx +14 -0
  35. package/views/Region.d.ts +20 -0
  36. package/views/Region.jsx +57 -0
  37. package/views/RichTextView.d.ts +15 -0
  38. package/views/RichTextView.jsx +55 -0
  39. package/views/SingleComponent.d.ts +5 -0
  40. package/views/SingleComponent.jsx +11 -0
  41. package/views/Text.d.ts +9 -0
  42. package/views/Text.jsx +4 -0
  43. package/views/macros/DefaultMacro.d.ts +4 -0
  44. package/views/macros/DefaultMacro.jsx +3 -0
  45. package/views/macros/DisableMacro.d.ts +3 -0
  46. 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
@@ -0,0 +1,2 @@
1
+ export * from './utils';
2
+ export * from './guillotine/fetchContent';
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './utils';
2
+ export * from './guillotine/fetchContent';
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,4 @@
1
+ /// <reference types="react" />
2
+ import { FetchContentResult } from '../guillotine/fetchContent';
3
+ declare const BaseContent: (props: FetchContentResult) => JSX.Element;
4
+ export default BaseContent;
@@ -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;