@terminalfour/terminalfour-js 1.0.0-rc.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/LICENSE.md +106 -0
- package/README.md +169 -0
- package/dist/cjs/element-resolver.d.ts +115 -0
- package/dist/cjs/element-resolver.d.ts.map +1 -0
- package/dist/cjs/element-resolver.js +391 -0
- package/dist/cjs/element-resolver.js.map +1 -0
- package/dist/cjs/errors.d.ts +21 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +21 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/handlebars.d.ts +123 -0
- package/dist/cjs/handlebars.d.ts.map +1 -0
- package/dist/cjs/handlebars.js +306 -0
- package/dist/cjs/handlebars.js.map +1 -0
- package/dist/cjs/http-client.d.ts +21 -0
- package/dist/cjs/http-client.d.ts.map +1 -0
- package/dist/cjs/http-client.js +126 -0
- package/dist/cjs/http-client.js.map +1 -0
- package/dist/cjs/index.d.ts +37 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +55 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/media-category-ref.d.ts +78 -0
- package/dist/cjs/media-category-ref.d.ts.map +1 -0
- package/dist/cjs/media-category-ref.js +184 -0
- package/dist/cjs/media-category-ref.js.map +1 -0
- package/dist/cjs/media-library.d.ts +30 -0
- package/dist/cjs/media-library.d.ts.map +1 -0
- package/dist/cjs/media-library.js +77 -0
- package/dist/cjs/media-library.js.map +1 -0
- package/dist/cjs/models/content-item.d.ts +80 -0
- package/dist/cjs/models/content-item.d.ts.map +1 -0
- package/dist/cjs/models/content-item.js +682 -0
- package/dist/cjs/models/content-item.js.map +1 -0
- package/dist/cjs/models/media-category-item.d.ts +29 -0
- package/dist/cjs/models/media-category-item.d.ts.map +1 -0
- package/dist/cjs/models/media-category-item.js +33 -0
- package/dist/cjs/models/media-category-item.js.map +1 -0
- package/dist/cjs/models/media-item.d.ts +74 -0
- package/dist/cjs/models/media-item.d.ts.map +1 -0
- package/dist/cjs/models/media-item.js +188 -0
- package/dist/cjs/models/media-item.js.map +1 -0
- package/dist/cjs/models/section-item.d.ts +58 -0
- package/dist/cjs/models/section-item.d.ts.map +1 -0
- package/dist/cjs/models/section-item.js +166 -0
- package/dist/cjs/models/section-item.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/resources/channel-resource.d.ts +112 -0
- package/dist/cjs/resources/channel-resource.d.ts.map +1 -0
- package/dist/cjs/resources/channel-resource.js +107 -0
- package/dist/cjs/resources/channel-resource.js.map +1 -0
- package/dist/cjs/resources/content-resource.d.ts +50 -0
- package/dist/cjs/resources/content-resource.d.ts.map +1 -0
- package/dist/cjs/resources/content-resource.js +286 -0
- package/dist/cjs/resources/content-resource.js.map +1 -0
- package/dist/cjs/resources/content-type-resource.d.ts +283 -0
- package/dist/cjs/resources/content-type-resource.d.ts.map +1 -0
- package/dist/cjs/resources/content-type-resource.js +970 -0
- package/dist/cjs/resources/content-type-resource.js.map +1 -0
- package/dist/cjs/resources/group-resource.d.ts +96 -0
- package/dist/cjs/resources/group-resource.d.ts.map +1 -0
- package/dist/cjs/resources/group-resource.js +213 -0
- package/dist/cjs/resources/group-resource.js.map +1 -0
- package/dist/cjs/resources/list-resource.d.ts +111 -0
- package/dist/cjs/resources/list-resource.d.ts.map +1 -0
- package/dist/cjs/resources/list-resource.js +179 -0
- package/dist/cjs/resources/list-resource.js.map +1 -0
- package/dist/cjs/resources/media-resource.d.ts +69 -0
- package/dist/cjs/resources/media-resource.d.ts.map +1 -0
- package/dist/cjs/resources/media-resource.js +210 -0
- package/dist/cjs/resources/media-resource.js.map +1 -0
- package/dist/cjs/resources/media-type-resource.d.ts +70 -0
- package/dist/cjs/resources/media-type-resource.d.ts.map +1 -0
- package/dist/cjs/resources/media-type-resource.js +195 -0
- package/dist/cjs/resources/media-type-resource.js.map +1 -0
- package/dist/cjs/resources/navigation-resource.d.ts +664 -0
- package/dist/cjs/resources/navigation-resource.d.ts.map +1 -0
- package/dist/cjs/resources/navigation-resource.js +2349 -0
- package/dist/cjs/resources/navigation-resource.js.map +1 -0
- package/dist/cjs/resources/page-layout-resource.d.ts +83 -0
- package/dist/cjs/resources/page-layout-resource.d.ts.map +1 -0
- package/dist/cjs/resources/page-layout-resource.js +214 -0
- package/dist/cjs/resources/page-layout-resource.js.map +1 -0
- package/dist/cjs/resources/user-resource.d.ts +126 -0
- package/dist/cjs/resources/user-resource.d.ts.map +1 -0
- package/dist/cjs/resources/user-resource.js +317 -0
- package/dist/cjs/resources/user-resource.js.map +1 -0
- package/dist/cjs/section-ref.d.ts +185 -0
- package/dist/cjs/section-ref.d.ts.map +1 -0
- package/dist/cjs/section-ref.js +813 -0
- package/dist/cjs/section-ref.js.map +1 -0
- package/dist/cjs/site-structure.d.ts +18 -0
- package/dist/cjs/site-structure.d.ts.map +1 -0
- package/dist/cjs/site-structure.js +57 -0
- package/dist/cjs/site-structure.js.map +1 -0
- package/dist/cjs/t4-client.d.ts +121 -0
- package/dist/cjs/t4-client.d.ts.map +1 -0
- package/dist/cjs/t4-client.js +190 -0
- package/dist/cjs/t4-client.js.map +1 -0
- package/dist/cjs/type-registry.d.ts +31 -0
- package/dist/cjs/type-registry.d.ts.map +1 -0
- package/dist/cjs/type-registry.js +76 -0
- package/dist/cjs/type-registry.js.map +1 -0
- package/dist/cjs/types.d.ts +211 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils.d.ts +147 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/utils.js +408 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/esm/element-resolver.d.ts +115 -0
- package/dist/esm/element-resolver.d.ts.map +1 -0
- package/dist/esm/element-resolver.js +387 -0
- package/dist/esm/element-resolver.js.map +1 -0
- package/dist/esm/errors.d.ts +21 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +17 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/handlebars.d.ts +123 -0
- package/dist/esm/handlebars.d.ts.map +1 -0
- package/dist/esm/handlebars.js +300 -0
- package/dist/esm/handlebars.js.map +1 -0
- package/dist/esm/http-client.d.ts +21 -0
- package/dist/esm/http-client.d.ts.map +1 -0
- package/dist/esm/http-client.js +122 -0
- package/dist/esm/http-client.js.map +1 -0
- package/dist/esm/index.d.ts +37 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/media-category-ref.d.ts +78 -0
- package/dist/esm/media-category-ref.d.ts.map +1 -0
- package/dist/esm/media-category-ref.js +180 -0
- package/dist/esm/media-category-ref.js.map +1 -0
- package/dist/esm/media-library.d.ts +30 -0
- package/dist/esm/media-library.d.ts.map +1 -0
- package/dist/esm/media-library.js +73 -0
- package/dist/esm/media-library.js.map +1 -0
- package/dist/esm/models/content-item.d.ts +80 -0
- package/dist/esm/models/content-item.d.ts.map +1 -0
- package/dist/esm/models/content-item.js +676 -0
- package/dist/esm/models/content-item.js.map +1 -0
- package/dist/esm/models/media-category-item.d.ts +29 -0
- package/dist/esm/models/media-category-item.d.ts.map +1 -0
- package/dist/esm/models/media-category-item.js +29 -0
- package/dist/esm/models/media-category-item.js.map +1 -0
- package/dist/esm/models/media-item.d.ts +74 -0
- package/dist/esm/models/media-item.d.ts.map +1 -0
- package/dist/esm/models/media-item.js +184 -0
- package/dist/esm/models/media-item.js.map +1 -0
- package/dist/esm/models/section-item.d.ts +58 -0
- package/dist/esm/models/section-item.d.ts.map +1 -0
- package/dist/esm/models/section-item.js +162 -0
- package/dist/esm/models/section-item.js.map +1 -0
- package/dist/esm/resources/channel-resource.d.ts +112 -0
- package/dist/esm/resources/channel-resource.d.ts.map +1 -0
- package/dist/esm/resources/channel-resource.js +102 -0
- package/dist/esm/resources/channel-resource.js.map +1 -0
- package/dist/esm/resources/content-resource.d.ts +50 -0
- package/dist/esm/resources/content-resource.d.ts.map +1 -0
- package/dist/esm/resources/content-resource.js +282 -0
- package/dist/esm/resources/content-resource.js.map +1 -0
- package/dist/esm/resources/content-type-resource.d.ts +283 -0
- package/dist/esm/resources/content-type-resource.d.ts.map +1 -0
- package/dist/esm/resources/content-type-resource.js +964 -0
- package/dist/esm/resources/content-type-resource.js.map +1 -0
- package/dist/esm/resources/group-resource.d.ts +96 -0
- package/dist/esm/resources/group-resource.d.ts.map +1 -0
- package/dist/esm/resources/group-resource.js +208 -0
- package/dist/esm/resources/group-resource.js.map +1 -0
- package/dist/esm/resources/list-resource.d.ts +111 -0
- package/dist/esm/resources/list-resource.d.ts.map +1 -0
- package/dist/esm/resources/list-resource.js +174 -0
- package/dist/esm/resources/list-resource.js.map +1 -0
- package/dist/esm/resources/media-resource.d.ts +69 -0
- package/dist/esm/resources/media-resource.d.ts.map +1 -0
- package/dist/esm/resources/media-resource.js +206 -0
- package/dist/esm/resources/media-resource.js.map +1 -0
- package/dist/esm/resources/media-type-resource.d.ts +70 -0
- package/dist/esm/resources/media-type-resource.d.ts.map +1 -0
- package/dist/esm/resources/media-type-resource.js +190 -0
- package/dist/esm/resources/media-type-resource.js.map +1 -0
- package/dist/esm/resources/navigation-resource.d.ts +664 -0
- package/dist/esm/resources/navigation-resource.d.ts.map +1 -0
- package/dist/esm/resources/navigation-resource.js +2344 -0
- package/dist/esm/resources/navigation-resource.js.map +1 -0
- package/dist/esm/resources/page-layout-resource.d.ts +83 -0
- package/dist/esm/resources/page-layout-resource.d.ts.map +1 -0
- package/dist/esm/resources/page-layout-resource.js +209 -0
- package/dist/esm/resources/page-layout-resource.js.map +1 -0
- package/dist/esm/resources/user-resource.d.ts +126 -0
- package/dist/esm/resources/user-resource.d.ts.map +1 -0
- package/dist/esm/resources/user-resource.js +312 -0
- package/dist/esm/resources/user-resource.js.map +1 -0
- package/dist/esm/section-ref.d.ts +185 -0
- package/dist/esm/section-ref.d.ts.map +1 -0
- package/dist/esm/section-ref.js +808 -0
- package/dist/esm/section-ref.js.map +1 -0
- package/dist/esm/site-structure.d.ts +18 -0
- package/dist/esm/site-structure.d.ts.map +1 -0
- package/dist/esm/site-structure.js +53 -0
- package/dist/esm/site-structure.js.map +1 -0
- package/dist/esm/t4-client.d.ts +121 -0
- package/dist/esm/t4-client.d.ts.map +1 -0
- package/dist/esm/t4-client.js +186 -0
- package/dist/esm/t4-client.js.map +1 -0
- package/dist/esm/type-registry.d.ts +31 -0
- package/dist/esm/type-registry.d.ts.map +1 -0
- package/dist/esm/type-registry.js +72 -0
- package/dist/esm/type-registry.js.map +1 -0
- package/dist/esm/types.d.ts +211 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils.d.ts +147 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +355 -0
- package/dist/esm/utils.js.map +1 -0
- package/docs/channels.md +62 -0
- package/docs/content-types.md +313 -0
- package/docs/content.md +199 -0
- package/docs/error-handling.md +86 -0
- package/docs/getting-started.md +146 -0
- package/docs/groups-and-users.md +167 -0
- package/docs/handlebars.md +145 -0
- package/docs/lists.md +98 -0
- package/docs/media-types.md +111 -0
- package/docs/media.md +169 -0
- package/docs/navigation/a-to-z.md +62 -0
- package/docs/navigation/breadcrumbs.md +67 -0
- package/docs/navigation/css-selector.md +53 -0
- package/docs/navigation/generate-file.md +48 -0
- package/docs/navigation/keyword-search.md +134 -0
- package/docs/navigation/language-switcher.md +37 -0
- package/docs/navigation/link-menu.md +105 -0
- package/docs/navigation/pagination.md +80 -0
- package/docs/navigation/previous-next-fulltext.md +43 -0
- package/docs/navigation/publish-to-one-file.md +93 -0
- package/docs/navigation/related-content.md +79 -0
- package/docs/navigation/related-section-branch.md +31 -0
- package/docs/navigation/return-to-index.md +35 -0
- package/docs/navigation/section-details.md +51 -0
- package/docs/navigation/section-iterator.md +33 -0
- package/docs/navigation/section-meta-info.md +39 -0
- package/docs/navigation/site-map.md +58 -0
- package/docs/navigation/top-content.md +80 -0
- package/docs/navigation/top-stories.md +47 -0
- package/docs/navigation.md +134 -0
- package/docs/page-layouts.md +71 -0
- package/docs/sections.md +224 -0
- package/docs/typescript.md +124 -0
- package/package.json +64 -0
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContentTypeResource = exports.ContentType = exports.Layout = void 0;
|
|
4
|
+
const utils_js_1 = require("../utils.js");
|
|
5
|
+
/** The content type category ID that marks a system content type. */
|
|
6
|
+
const SYSTEM_CONTENT_TYPE = 30;
|
|
7
|
+
function resolveTypeName(type, typeMap) {
|
|
8
|
+
if (type == null)
|
|
9
|
+
return '';
|
|
10
|
+
if (typeof type === 'number')
|
|
11
|
+
return typeMap.get(type) ?? `Type ${type}`;
|
|
12
|
+
if (typeof type === 'object' && type !== null && 'name' in type)
|
|
13
|
+
return type.name ?? '';
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Maps a raw API content type response to a ContentTypeData object.
|
|
18
|
+
*/
|
|
19
|
+
function mapContentType(raw, typeMap, editorMap) {
|
|
20
|
+
const fieldsRecord = {};
|
|
21
|
+
for (const el of raw.contentTypeElements ?? []) {
|
|
22
|
+
const friendlyName = (0, utils_js_1.decodeHtmlEntities)(el.alias || el.name);
|
|
23
|
+
const field = {
|
|
24
|
+
name: friendlyName,
|
|
25
|
+
description: (0, utils_js_1.decodeHtmlEntities)(el.description ?? ''),
|
|
26
|
+
type: resolveTypeName(el.type, typeMap),
|
|
27
|
+
required: el.compulsory ?? false,
|
|
28
|
+
maxSize: el.maxSize ?? 0,
|
|
29
|
+
listId: el.listId ?? 0,
|
|
30
|
+
listName: '',
|
|
31
|
+
shown: el.shown ?? true,
|
|
32
|
+
useAsFilename: raw.elementIdforFilename !== undefined && el.id === raw.elementIdforFilename,
|
|
33
|
+
};
|
|
34
|
+
if (el.contentTypeElementConfiguration) {
|
|
35
|
+
const cfg = el.contentTypeElementConfiguration;
|
|
36
|
+
const elTypeName = resolveTypeName(el.type, typeMap);
|
|
37
|
+
if (elTypeName === 'Repeater') {
|
|
38
|
+
field.config = {
|
|
39
|
+
contentTypeId: cfg.contentTypeId ?? 0,
|
|
40
|
+
contentTypeName: cfg.contentTypeDTO?.alias || cfg.contentTypeDTO?.name || '',
|
|
41
|
+
contentTypeDescription: cfg.contentTypeDTO?.description ?? '',
|
|
42
|
+
layout: cfg.layout ?? '',
|
|
43
|
+
minRepeats: cfg.minRepeats ?? 0,
|
|
44
|
+
maxRepeats: cfg.maxRepeats ?? 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
else if (elTypeName === 'HTML' && cfg.editorID != null && editorMap) {
|
|
48
|
+
field.editor = editorMap.get(cfg.editorID) ?? `Editor ${cfg.editorID}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Store element ID as non-enumerable for internal matching on save
|
|
52
|
+
Object.defineProperty(field, '_elementId', { value: el.id, enumerable: false });
|
|
53
|
+
fieldsRecord[friendlyName] = field;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
data: {
|
|
57
|
+
id: raw.id,
|
|
58
|
+
name: (0, utils_js_1.decodeHtmlEntities)(raw.alias || raw.name),
|
|
59
|
+
description: (0, utils_js_1.decodeHtmlEntities)(raw.description ?? ''),
|
|
60
|
+
minUserLevel: utils_js_1.AUTH_LEVEL_MAP[raw.minAuthLevel ?? 2] ?? `unknown (${raw.minAuthLevel})`,
|
|
61
|
+
workflow: raw.workflow ?? 0,
|
|
62
|
+
sharedGroups: (raw.sharedGroups ?? []).map((g) => g.id),
|
|
63
|
+
primaryGroup: raw.primaryGroup?.group?.id ?? raw.primaryGroup?.id ?? 0,
|
|
64
|
+
directEdit: raw.enableDirectEdit ?? true,
|
|
65
|
+
fields: fieldsRecord,
|
|
66
|
+
},
|
|
67
|
+
rawData: raw,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Friendly processor names mapped to API names */
|
|
71
|
+
const PROCESSOR_MAP = {
|
|
72
|
+
't4-tags': 'T4 Tag Content',
|
|
73
|
+
'handlebars': 'Handlebars Content',
|
|
74
|
+
'programmable-layouts': 'Programmable Layout Content',
|
|
75
|
+
};
|
|
76
|
+
/** Extracts a value from raw elements by element name prefix (case-insensitive). */
|
|
77
|
+
function getElementValue(elements, name) {
|
|
78
|
+
const key = Object.keys(elements).find((k) => k.toLowerCase().startsWith(name.toLowerCase() + '#'));
|
|
79
|
+
return key ? String(elements[key] ?? '') : '';
|
|
80
|
+
}
|
|
81
|
+
/** A mutable content layout. Modify properties and call save() to persist. */
|
|
82
|
+
class Layout {
|
|
83
|
+
constructor(raw, httpClient, _contentTypeId, layoutElements, fetchLayouts, _resolveSyntaxId, _resolveProcessorId, _resolveExtension) {
|
|
84
|
+
this.name = raw.name;
|
|
85
|
+
this.code = getElementValue(raw.elements, 'formatcode');
|
|
86
|
+
this.lastModified = raw.lastModified ? new Date(raw.lastModified) : null;
|
|
87
|
+
Object.defineProperty(this, '_httpClient', { value: httpClient, enumerable: false });
|
|
88
|
+
Object.defineProperty(this, '_rawDTO', { value: raw, enumerable: false, writable: true });
|
|
89
|
+
Object.defineProperty(this, '_layoutElements', { value: layoutElements, enumerable: false });
|
|
90
|
+
Object.defineProperty(this, '_fetchLayouts', { value: fetchLayouts, enumerable: false });
|
|
91
|
+
}
|
|
92
|
+
/** Persists current property values to the server. */
|
|
93
|
+
async save() {
|
|
94
|
+
// If name changed, check uniqueness
|
|
95
|
+
if (this.name !== this._rawDTO.name) {
|
|
96
|
+
const existing = await this._fetchLayouts();
|
|
97
|
+
if (existing.some((l) => l.name === this.name && l.id !== this._rawDTO.id)) {
|
|
98
|
+
throw new Error(`Layout "${this.name}" already exists on this content type`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const findEl = (n) => {
|
|
102
|
+
const el = this._layoutElements.find((e) => e.name.toLowerCase() === n.toLowerCase());
|
|
103
|
+
if (!el)
|
|
104
|
+
throw new Error(`Layout element "${n}" not found`);
|
|
105
|
+
return el;
|
|
106
|
+
};
|
|
107
|
+
const nameEl = findEl('name');
|
|
108
|
+
const codeEl = findEl('formatcode');
|
|
109
|
+
// Start from raw elements, overlay changed fields
|
|
110
|
+
const elements = { ...this._rawDTO.elements };
|
|
111
|
+
elements[layoutKey(nameEl)] = this.name;
|
|
112
|
+
elements[layoutKey(codeEl)] = this.code;
|
|
113
|
+
const response = await this._httpClient.request({
|
|
114
|
+
method: 'PUT',
|
|
115
|
+
path: `/layout/${this._rawDTO.id}/en`,
|
|
116
|
+
body: {
|
|
117
|
+
...this._rawDTO,
|
|
118
|
+
name: this.name,
|
|
119
|
+
elements,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
this._rawDTO = response;
|
|
123
|
+
this.name = response.name;
|
|
124
|
+
this.code = getElementValue(response.elements, 'formatcode');
|
|
125
|
+
this.lastModified = response.lastModified ? new Date(response.lastModified) : null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
exports.Layout = Layout;
|
|
129
|
+
/** Builds an element key from a layout element: Name#id:type */
|
|
130
|
+
function layoutKey(el) {
|
|
131
|
+
return `${el.name}#${el.id}:${el.type}`;
|
|
132
|
+
}
|
|
133
|
+
/** A content type with methods for related operations. */
|
|
134
|
+
class ContentType {
|
|
135
|
+
constructor(data, httpClient, rawData) {
|
|
136
|
+
this.id = data.id;
|
|
137
|
+
this.name = data.name;
|
|
138
|
+
this.description = data.description;
|
|
139
|
+
this.minUserLevel = data.minUserLevel;
|
|
140
|
+
this.workflow = data.workflow;
|
|
141
|
+
this.sharedGroups = data.sharedGroups;
|
|
142
|
+
this.primaryGroup = data.primaryGroup;
|
|
143
|
+
this.directEdit = data.directEdit;
|
|
144
|
+
this.fields = data.fields;
|
|
145
|
+
Object.defineProperty(this, '_httpClient', { value: httpClient, enumerable: false });
|
|
146
|
+
Object.defineProperty(this, '_rawData', { value: rawData ?? {}, enumerable: false, writable: true });
|
|
147
|
+
Object.defineProperty(this, '_removedElementIds', { value: new Set(), enumerable: false, writable: true });
|
|
148
|
+
Object.defineProperty(this, '_exemptSystemTypeIdsCache', { value: null, enumerable: false, writable: true });
|
|
149
|
+
const contentTypeId = this.id;
|
|
150
|
+
let layoutElementsCache = null;
|
|
151
|
+
let layoutContentTypeIdCache = null;
|
|
152
|
+
const fetchLayouts = async () => {
|
|
153
|
+
return httpClient.request({
|
|
154
|
+
method: 'GET',
|
|
155
|
+
path: `/layout/contenttype/${contentTypeId}/en`,
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
/** Resolves the layout content type ID by looking up "Content Layout" (type 30). */
|
|
159
|
+
const getLayoutContentTypeId = async () => {
|
|
160
|
+
if (layoutContentTypeIdCache !== null)
|
|
161
|
+
return layoutContentTypeIdCache;
|
|
162
|
+
const types = await httpClient.request({
|
|
163
|
+
method: 'GET',
|
|
164
|
+
path: '/contenttype/?excludeElements=true',
|
|
165
|
+
});
|
|
166
|
+
const match = types.find((t) => (t.name === 'Content Layout' || t.alias === 'Content Layout') && t.type === 30);
|
|
167
|
+
if (!match)
|
|
168
|
+
throw new Error('Could not find "Content Layout" system content type');
|
|
169
|
+
layoutContentTypeIdCache = match.id;
|
|
170
|
+
return match.id;
|
|
171
|
+
};
|
|
172
|
+
/** Fetches and caches the layout content type elements. */
|
|
173
|
+
const getLayoutElements = async () => {
|
|
174
|
+
if (layoutElementsCache)
|
|
175
|
+
return layoutElementsCache;
|
|
176
|
+
const ctId = await getLayoutContentTypeId();
|
|
177
|
+
const raw = await httpClient.request({
|
|
178
|
+
method: 'GET',
|
|
179
|
+
path: `/contenttype/${ctId}`,
|
|
180
|
+
});
|
|
181
|
+
layoutElementsCache = raw.contentTypeElements ?? [];
|
|
182
|
+
return layoutElementsCache;
|
|
183
|
+
};
|
|
184
|
+
/** Finds a layout element by name (case-insensitive). */
|
|
185
|
+
const findElement = (elements, name) => {
|
|
186
|
+
const el = elements.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
187
|
+
if (!el)
|
|
188
|
+
throw new Error(`Layout element "${name}" not found in layout content type`);
|
|
189
|
+
return el;
|
|
190
|
+
};
|
|
191
|
+
const resolveSyntaxId = async (syntaxName) => {
|
|
192
|
+
const types = await httpClient.request({
|
|
193
|
+
method: 'GET',
|
|
194
|
+
path: '/syntaxType',
|
|
195
|
+
});
|
|
196
|
+
const match = types.find((t) => t.name.toLowerCase() === syntaxName.toLowerCase());
|
|
197
|
+
if (!match) {
|
|
198
|
+
const valid = types.map((t) => t.name).join(', ');
|
|
199
|
+
throw new Error(`Unknown syntax "${syntaxName}". Valid options: ${valid}`);
|
|
200
|
+
}
|
|
201
|
+
return String(match.id);
|
|
202
|
+
};
|
|
203
|
+
const resolveProcessorId = async (processorKey) => {
|
|
204
|
+
const apiName = PROCESSOR_MAP[processorKey];
|
|
205
|
+
if (!apiName) {
|
|
206
|
+
throw new Error(`Unknown processor "${processorKey}". Valid options: t4-tags, handlebars, programmable-layouts`);
|
|
207
|
+
}
|
|
208
|
+
const processors = await httpClient.request({
|
|
209
|
+
method: 'GET',
|
|
210
|
+
path: '/publishProcessor/20',
|
|
211
|
+
});
|
|
212
|
+
const match = processors.find((p) => p.name === apiName);
|
|
213
|
+
if (!match)
|
|
214
|
+
throw new Error(`Processor "${apiName}" not found on this T4 instance`);
|
|
215
|
+
return String(match.id);
|
|
216
|
+
};
|
|
217
|
+
const resolveExtension = async (ext) => {
|
|
218
|
+
const extensions = await httpClient.request({
|
|
219
|
+
method: 'GET',
|
|
220
|
+
path: '/fileExtension',
|
|
221
|
+
});
|
|
222
|
+
const match = extensions.find((e) => e.extension.toLowerCase() === ext.toLowerCase());
|
|
223
|
+
if (!match) {
|
|
224
|
+
const valid = extensions.map((e) => e.extension).join(', ');
|
|
225
|
+
throw new Error(`Unknown extension "${ext}". Valid options: ${valid}`);
|
|
226
|
+
}
|
|
227
|
+
return match.extension;
|
|
228
|
+
};
|
|
229
|
+
Object.defineProperty(this, 'layouts', {
|
|
230
|
+
value: {
|
|
231
|
+
list: async () => {
|
|
232
|
+
const raw = await fetchLayouts();
|
|
233
|
+
return raw.map((l) => ({
|
|
234
|
+
name: l.name,
|
|
235
|
+
lastModified: l.lastModified ? new Date(l.lastModified) : null,
|
|
236
|
+
}));
|
|
237
|
+
},
|
|
238
|
+
get: async (layoutName) => {
|
|
239
|
+
const all = await fetchLayouts();
|
|
240
|
+
const match = all.find((l) => l.name === layoutName);
|
|
241
|
+
if (!match)
|
|
242
|
+
throw new Error(`Layout "${layoutName}" not found on this content type`);
|
|
243
|
+
// Fetch full layout details
|
|
244
|
+
const raw = await httpClient.request({
|
|
245
|
+
method: 'GET',
|
|
246
|
+
path: `/layout/${match.id}/en`,
|
|
247
|
+
});
|
|
248
|
+
const layoutElements = await getLayoutElements();
|
|
249
|
+
return new Layout(raw, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
|
|
250
|
+
},
|
|
251
|
+
update: async (layoutName, updateData) => {
|
|
252
|
+
const all = await fetchLayouts();
|
|
253
|
+
const match = all.find((l) => l.name === layoutName);
|
|
254
|
+
if (!match)
|
|
255
|
+
throw new Error(`Layout "${layoutName}" not found on this content type`);
|
|
256
|
+
// If renaming, check uniqueness
|
|
257
|
+
if (updateData.name && updateData.name !== layoutName) {
|
|
258
|
+
if (all.some((l) => l.name === updateData.name)) {
|
|
259
|
+
throw new Error(`Layout "${updateData.name}" already exists on this content type`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Fetch full layout
|
|
263
|
+
const raw = await httpClient.request({
|
|
264
|
+
method: 'GET',
|
|
265
|
+
path: `/layout/${match.id}/en`,
|
|
266
|
+
});
|
|
267
|
+
const layoutElements = await getLayoutElements();
|
|
268
|
+
const elements = { ...raw.elements };
|
|
269
|
+
const newName = updateData.name ?? raw.name;
|
|
270
|
+
elements[layoutKey(findElement(layoutElements, 'name'))] = newName;
|
|
271
|
+
if (updateData.code !== undefined) {
|
|
272
|
+
elements[layoutKey(findElement(layoutElements, 'formatcode'))] = updateData.code;
|
|
273
|
+
}
|
|
274
|
+
if (updateData.syntax !== undefined) {
|
|
275
|
+
elements[layoutKey(findElement(layoutElements, 'syntax'))] = await resolveSyntaxId(updateData.syntax);
|
|
276
|
+
}
|
|
277
|
+
if (updateData.processor !== undefined) {
|
|
278
|
+
elements[layoutKey(findElement(layoutElements, 'Format Processor'))] = await resolveProcessorId(updateData.processor);
|
|
279
|
+
}
|
|
280
|
+
if (updateData.extension !== undefined) {
|
|
281
|
+
const extValue = updateData.extension ? await resolveExtension(updateData.extension) : '';
|
|
282
|
+
elements[layoutKey(findElement(layoutElements, 'extension'))] = extValue;
|
|
283
|
+
}
|
|
284
|
+
const response = await httpClient.request({
|
|
285
|
+
method: 'PUT',
|
|
286
|
+
path: `/layout/${match.id}/en`,
|
|
287
|
+
body: { ...raw, name: newName, elements },
|
|
288
|
+
});
|
|
289
|
+
return new Layout(response, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
|
|
290
|
+
},
|
|
291
|
+
create: async (createData) => {
|
|
292
|
+
// Check name uniqueness
|
|
293
|
+
const existing = await fetchLayouts();
|
|
294
|
+
if (existing.some((l) => l.name === createData.name)) {
|
|
295
|
+
throw new Error(`Layout "${createData.name}" already exists on this content type`);
|
|
296
|
+
}
|
|
297
|
+
// Fetch layout content type elements for dynamic key resolution
|
|
298
|
+
const layoutElements = await getLayoutElements();
|
|
299
|
+
const layoutCtId = await getLayoutContentTypeId();
|
|
300
|
+
const nameEl = findElement(layoutElements, 'name');
|
|
301
|
+
const codeEl = findElement(layoutElements, 'formatcode');
|
|
302
|
+
const extensionEl = findElement(layoutElements, 'extension');
|
|
303
|
+
const syntaxEl = findElement(layoutElements, 'syntax');
|
|
304
|
+
const processorEl = findElement(layoutElements, 'Format Processor');
|
|
305
|
+
const [syntaxId, processorId] = await Promise.all([
|
|
306
|
+
resolveSyntaxId(createData.syntax ?? 'HTML/XML'),
|
|
307
|
+
resolveProcessorId(createData.processor ?? 'handlebars'),
|
|
308
|
+
]);
|
|
309
|
+
const extensionValue = createData.extension
|
|
310
|
+
? await resolveExtension(createData.extension)
|
|
311
|
+
: '';
|
|
312
|
+
const response = await httpClient.request({
|
|
313
|
+
method: 'PUT',
|
|
314
|
+
path: `/layout/${contentTypeId}`,
|
|
315
|
+
body: {
|
|
316
|
+
id: 0,
|
|
317
|
+
contentTypeID: layoutCtId,
|
|
318
|
+
archiveSection: 0,
|
|
319
|
+
name: createData.name,
|
|
320
|
+
status: 0,
|
|
321
|
+
lastModifiedBy: 0,
|
|
322
|
+
channels: [],
|
|
323
|
+
editable: false,
|
|
324
|
+
expired: false,
|
|
325
|
+
canPublishNow: false,
|
|
326
|
+
canSaveAndApprove: false,
|
|
327
|
+
contentTypeAccess: 0,
|
|
328
|
+
elements: {
|
|
329
|
+
[layoutKey(nameEl)]: createData.name,
|
|
330
|
+
[layoutKey(extensionEl)]: extensionValue,
|
|
331
|
+
[layoutKey(syntaxEl)]: syntaxId,
|
|
332
|
+
[layoutKey(codeEl)]: createData.code,
|
|
333
|
+
[layoutKey(processorEl)]: processorId,
|
|
334
|
+
},
|
|
335
|
+
sectionIDs: [],
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
return new Layout(response, httpClient, contentTypeId, layoutElements, fetchLayouts, resolveSyntaxId, resolveProcessorId, resolveExtension);
|
|
339
|
+
},
|
|
340
|
+
delete: async (layoutName) => {
|
|
341
|
+
const all = await fetchLayouts();
|
|
342
|
+
const match = all.find((l) => l.name === layoutName);
|
|
343
|
+
if (!match)
|
|
344
|
+
throw new Error(`Layout "${layoutName}" not found on this content type`);
|
|
345
|
+
await httpClient.request({
|
|
346
|
+
method: 'DELETE',
|
|
347
|
+
path: `/layout/${match.id}/en`,
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
enumerable: false,
|
|
352
|
+
configurable: false,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Adds a new field (element) to this content type. Takes effect on save().
|
|
357
|
+
*
|
|
358
|
+
* The `type` must be a valid element type name (e.g. 'Plain Text', 'HTML', 'Select Box', 'Repeater').
|
|
359
|
+
* Use `t4.contentTypes.get(id)` to see existing field types for reference.
|
|
360
|
+
*/
|
|
361
|
+
async addField(data) {
|
|
362
|
+
if (!data.name?.trim())
|
|
363
|
+
throw new Error('Field name is required');
|
|
364
|
+
if (!data.type?.trim())
|
|
365
|
+
throw new Error('Field type is required');
|
|
366
|
+
if (this.fields[data.name])
|
|
367
|
+
throw new Error(`Field "${data.name}" already exists on this content type`);
|
|
368
|
+
// Resolve type name to ID via GET /type/
|
|
369
|
+
const types = await this._httpClient.request({
|
|
370
|
+
method: 'GET',
|
|
371
|
+
path: '/type/',
|
|
372
|
+
});
|
|
373
|
+
const typeMatch = types.find((t) => t.name.toLowerCase() === data.type.toLowerCase());
|
|
374
|
+
if (!typeMatch) {
|
|
375
|
+
const valid = types.map((t) => t.name).join(', ');
|
|
376
|
+
throw new Error(`Unknown element type "${data.type}". Valid options: ${valid}`);
|
|
377
|
+
}
|
|
378
|
+
// Validate listId for list-based types
|
|
379
|
+
if (typeMatch.listType && !data.listId) {
|
|
380
|
+
throw new Error(`Field "${data.name}" is type "${data.type}" which requires a listId`);
|
|
381
|
+
}
|
|
382
|
+
// Validate repeater config
|
|
383
|
+
const isRepeater = typeMatch.name.toLowerCase() === 'repeater';
|
|
384
|
+
if (isRepeater && !data.repeater) {
|
|
385
|
+
throw new Error(`Field "${data.name}" is type "Repeater" which requires a repeater configuration (contentTypeId)`);
|
|
386
|
+
}
|
|
387
|
+
if (!isRepeater && data.repeater) {
|
|
388
|
+
throw new Error(`Field "${data.name}" is type "${data.type}" — repeater configuration is only valid for Repeater elements`);
|
|
389
|
+
}
|
|
390
|
+
// Validate editor option (only valid for HTML elements)
|
|
391
|
+
const isHtml = typeMatch.name === 'HTML';
|
|
392
|
+
let resolvedEditorId;
|
|
393
|
+
if (data.editor !== undefined) {
|
|
394
|
+
if (!isHtml) {
|
|
395
|
+
throw new Error(`Field "${data.name}" is type "${data.type}" — editor is only valid for HTML elements`);
|
|
396
|
+
}
|
|
397
|
+
const editors = await this._httpClient.request({
|
|
398
|
+
method: 'GET',
|
|
399
|
+
path: '/htmlEditor',
|
|
400
|
+
});
|
|
401
|
+
const match = editors.find((e) => e.name.toLowerCase() === data.editor.toLowerCase());
|
|
402
|
+
if (!match) {
|
|
403
|
+
const valid = editors.map((e) => `"${e.name}"`).join(', ');
|
|
404
|
+
throw new Error(`Unknown editor "${data.editor}". Valid options: ${valid}`);
|
|
405
|
+
}
|
|
406
|
+
resolvedEditorId = match.id;
|
|
407
|
+
}
|
|
408
|
+
// Generate a negative random ID for the new element (API convention for new items)
|
|
409
|
+
const newElementId = -Math.floor(Math.random() * 1000000);
|
|
410
|
+
// Determine the next sequence number
|
|
411
|
+
const existingElements = this._rawData.contentTypeElements ?? [];
|
|
412
|
+
const maxSequence = existingElements.reduce((max, el) => {
|
|
413
|
+
const seq = el.sequence;
|
|
414
|
+
return seq !== undefined && seq > max ? seq : max;
|
|
415
|
+
}, 0);
|
|
416
|
+
// Add to the raw elements array so save() picks it up.
|
|
417
|
+
// New elements use string type IDs and include contentTypeID (matching browser behaviour).
|
|
418
|
+
const newRawElement = {
|
|
419
|
+
id: newElementId,
|
|
420
|
+
name: data.name,
|
|
421
|
+
description: data.description ?? '',
|
|
422
|
+
type: String(typeMatch.id),
|
|
423
|
+
maxSize: data.maxSize ?? 80,
|
|
424
|
+
compulsory: data.required ?? false,
|
|
425
|
+
shown: data.shown ?? true,
|
|
426
|
+
contentTypeID: this.id,
|
|
427
|
+
sequence: maxSequence + 1,
|
|
428
|
+
};
|
|
429
|
+
// Add repeater configuration if applicable
|
|
430
|
+
if (isRepeater && data.repeater) {
|
|
431
|
+
const maxRepeats = data.repeater.maxRepeats ?? 100;
|
|
432
|
+
newRawElement.contentTypeElementConfiguration = {
|
|
433
|
+
contentTypeId: data.repeater.contentTypeId,
|
|
434
|
+
layout: data.repeater.layout ?? '',
|
|
435
|
+
minRepeats: data.repeater.minRepeats ?? 0,
|
|
436
|
+
maxRepeats: maxRepeats,
|
|
437
|
+
maxRepeatsLimit: maxRepeats,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
newRawElement.listId = data.listId ?? 0;
|
|
442
|
+
}
|
|
443
|
+
// Add HTML editor configuration if applicable
|
|
444
|
+
if (isHtml && resolvedEditorId !== undefined) {
|
|
445
|
+
newRawElement.contentTypeElementConfiguration = { editorID: resolvedEditorId };
|
|
446
|
+
}
|
|
447
|
+
existingElements.push(newRawElement);
|
|
448
|
+
// Add to the friendly fields record
|
|
449
|
+
const field = {
|
|
450
|
+
name: data.name,
|
|
451
|
+
description: data.description ?? '',
|
|
452
|
+
type: typeMatch.name,
|
|
453
|
+
required: data.required ?? false,
|
|
454
|
+
maxSize: data.maxSize ?? 80,
|
|
455
|
+
listId: data.listId ?? 0,
|
|
456
|
+
listName: '',
|
|
457
|
+
shown: data.shown ?? true,
|
|
458
|
+
useAsFilename: false,
|
|
459
|
+
};
|
|
460
|
+
if (isRepeater && data.repeater) {
|
|
461
|
+
field.config = {
|
|
462
|
+
contentTypeId: data.repeater.contentTypeId,
|
|
463
|
+
contentTypeName: '',
|
|
464
|
+
contentTypeDescription: '',
|
|
465
|
+
layout: data.repeater.layout ?? '',
|
|
466
|
+
minRepeats: data.repeater.minRepeats ?? 0,
|
|
467
|
+
maxRepeats: data.repeater.maxRepeats ?? 100,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (isHtml && data.editor) {
|
|
471
|
+
field.editor = data.editor;
|
|
472
|
+
}
|
|
473
|
+
Object.defineProperty(field, '_elementId', { value: newElementId, enumerable: false });
|
|
474
|
+
this.fields[data.name] = field;
|
|
475
|
+
}
|
|
476
|
+
/** Removes a field (element) by name. Takes effect on save(). */
|
|
477
|
+
removeField(fieldName) {
|
|
478
|
+
const field = this.fields[fieldName];
|
|
479
|
+
if (!field)
|
|
480
|
+
throw new Error(`Field "${fieldName}" not found on this content type`);
|
|
481
|
+
const elementId = field._elementId;
|
|
482
|
+
if (elementId !== undefined) {
|
|
483
|
+
this._removedElementIds.add(elementId);
|
|
484
|
+
}
|
|
485
|
+
delete this.fields[fieldName];
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Resolves the set of system content type IDs where removing and renaming
|
|
489
|
+
* elements is permitted: the Section Meta Data content type and the Extended
|
|
490
|
+
* User content type. Cached per instance; only called when a removal or rename
|
|
491
|
+
* is attempted on a system content type.
|
|
492
|
+
*/
|
|
493
|
+
async _getExemptSystemTypeIds() {
|
|
494
|
+
if (this._exemptSystemTypeIdsCache)
|
|
495
|
+
return this._exemptSystemTypeIdsCache;
|
|
496
|
+
const ids = new Set();
|
|
497
|
+
// Section Meta Data content type
|
|
498
|
+
try {
|
|
499
|
+
const config = await this._httpClient.request({
|
|
500
|
+
method: 'GET',
|
|
501
|
+
path: '/config/hierarchy.metaDataContentType',
|
|
502
|
+
});
|
|
503
|
+
const sectionMetaId = parseInt(config.value, 10);
|
|
504
|
+
if (sectionMetaId > 0)
|
|
505
|
+
ids.add(sectionMetaId);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
(0, utils_js_1.debugWarn)('Failed to resolve Section Meta Data content type ID', error);
|
|
509
|
+
}
|
|
510
|
+
// Extended User content type
|
|
511
|
+
try {
|
|
512
|
+
const credentials = await this._httpClient.request({
|
|
513
|
+
method: 'GET',
|
|
514
|
+
path: '/userSearch/credentials',
|
|
515
|
+
});
|
|
516
|
+
const userExtId = credentials.userExtensibleObjectID;
|
|
517
|
+
if (typeof userExtId === 'number' && userExtId > 0)
|
|
518
|
+
ids.add(userExtId);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
(0, utils_js_1.debugWarn)('Failed to resolve Extended User content type ID', error);
|
|
522
|
+
}
|
|
523
|
+
this._exemptSystemTypeIdsCache = ids;
|
|
524
|
+
return ids;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Guards against removing or renaming elements on system content types.
|
|
528
|
+
*
|
|
529
|
+
* System content types (`type === 30`) may have elements added, and their
|
|
530
|
+
* descriptions and maxSize changed, but removing or renaming existing elements
|
|
531
|
+
* is unsafe. Two system content types are exempt: the Section Meta Data content
|
|
532
|
+
* type and the Extended User content type.
|
|
533
|
+
*
|
|
534
|
+
* Detects renames by comparing each field's current name against the name/alias
|
|
535
|
+
* of its matched raw element. Only performs network calls when a removal or
|
|
536
|
+
* rename is actually attempted on a system content type.
|
|
537
|
+
*/
|
|
538
|
+
async _guardSystemContentTypeElementChanges(rawElements) {
|
|
539
|
+
if (this._rawData.type !== SYSTEM_CONTENT_TYPE)
|
|
540
|
+
return;
|
|
541
|
+
const removedNames = this._collectRemovedElementNames();
|
|
542
|
+
const renames = this._collectRenamedElements(rawElements);
|
|
543
|
+
if (removedNames.length === 0 && renames.length === 0)
|
|
544
|
+
return;
|
|
545
|
+
const exemptIds = await this._getExemptSystemTypeIds();
|
|
546
|
+
if (exemptIds.has(this.id))
|
|
547
|
+
return;
|
|
548
|
+
if (removedNames.length > 0) {
|
|
549
|
+
throw new Error(`Cannot remove element${removedNames.length > 1 ? 's' : ''} ${removedNames.map((n) => `"${n}"`).join(', ')} ` +
|
|
550
|
+
`from content type "${this.name}" because it is a system content type. ` +
|
|
551
|
+
'Removing elements from system content types is not allowed. ' +
|
|
552
|
+
'You can still add elements and change their description or maxSize.');
|
|
553
|
+
}
|
|
554
|
+
const { from, to } = renames[0];
|
|
555
|
+
throw new Error(`Cannot rename element "${from}" to "${to}" on content type "${this.name}" because it is a system content type. ` +
|
|
556
|
+
'Renaming elements on system content types is not allowed. ' +
|
|
557
|
+
'You can still add elements and change their description or maxSize.');
|
|
558
|
+
}
|
|
559
|
+
/** Returns the names of elements queued for removal, resolved from the raw data. */
|
|
560
|
+
_collectRemovedElementNames() {
|
|
561
|
+
if (this._removedElementIds.size === 0)
|
|
562
|
+
return [];
|
|
563
|
+
const names = [];
|
|
564
|
+
for (const el of this._rawData.contentTypeElements ?? []) {
|
|
565
|
+
if (el.id !== undefined && this._removedElementIds.has(el.id)) {
|
|
566
|
+
names.push((0, utils_js_1.decodeHtmlEntities)(el.alias || el.name));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return names;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Detects fields whose name differs from the current name/alias of their matched
|
|
573
|
+
* raw element (i.e. a rename). New fields (no matching raw element) are ignored.
|
|
574
|
+
*/
|
|
575
|
+
_collectRenamedElements(rawElements) {
|
|
576
|
+
const renames = [];
|
|
577
|
+
for (const field of Object.values(this.fields)) {
|
|
578
|
+
const elementId = field._elementId;
|
|
579
|
+
if (elementId === undefined)
|
|
580
|
+
continue;
|
|
581
|
+
const rawEl = rawElements.find((el) => el.id === elementId);
|
|
582
|
+
if (!rawEl)
|
|
583
|
+
continue;
|
|
584
|
+
const currentName = (0, utils_js_1.decodeHtmlEntities)(rawEl.alias || rawEl.name);
|
|
585
|
+
if (currentName !== field.name) {
|
|
586
|
+
renames.push({ from: currentName, to: field.name });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return renames;
|
|
590
|
+
}
|
|
591
|
+
/** Persists current property values to the server via PUT. */
|
|
592
|
+
async save() {
|
|
593
|
+
const authLevel = String(utils_js_1.AUTH_LEVEL_REVERSE[this.minUserLevel] ?? this._rawData.minAuthLevel ?? 2);
|
|
594
|
+
// Sync field changes back to raw contentTypeElements
|
|
595
|
+
let rawElements = (this._rawData.contentTypeElements ?? []);
|
|
596
|
+
// Block element removal/renaming on system content types (except the two
|
|
597
|
+
// exempt types). Runs before any mutation so nothing is persisted on failure.
|
|
598
|
+
await this._guardSystemContentTypeElementChanges(rawElements);
|
|
599
|
+
// Remove elements that were deleted via removeField()
|
|
600
|
+
if (this._removedElementIds.size > 0) {
|
|
601
|
+
rawElements = rawElements.filter((el) => !this._removedElementIds.has(el.id));
|
|
602
|
+
}
|
|
603
|
+
const editorFieldsToSync = [];
|
|
604
|
+
for (const field of Object.values(this.fields)) {
|
|
605
|
+
const elementId = field._elementId;
|
|
606
|
+
const rawEl = elementId !== undefined
|
|
607
|
+
? rawElements.find((el) => el.id === elementId)
|
|
608
|
+
: rawElements.find((el) => {
|
|
609
|
+
const elAlias = el.alias;
|
|
610
|
+
const elName = el.name;
|
|
611
|
+
return elAlias === field.name || elName === field.name;
|
|
612
|
+
});
|
|
613
|
+
if (rawEl) {
|
|
614
|
+
rawEl.alias = field.name;
|
|
615
|
+
rawEl.description = field.description;
|
|
616
|
+
rawEl.maxSize = field.maxSize;
|
|
617
|
+
rawEl.compulsory = field.required;
|
|
618
|
+
rawEl.shown = field.shown;
|
|
619
|
+
// Sync repeater config min/max if present (only for Repeater elements)
|
|
620
|
+
if (field.config && rawEl.contentTypeElementConfiguration && field.type === 'Repeater') {
|
|
621
|
+
const cfg = rawEl.contentTypeElementConfiguration;
|
|
622
|
+
cfg.minRepeats = field.config.minRepeats;
|
|
623
|
+
cfg.maxRepeats = field.config.maxRepeats;
|
|
624
|
+
}
|
|
625
|
+
// Sync HTML editor if changed
|
|
626
|
+
if (field.type === 'HTML' && field.editor !== undefined) {
|
|
627
|
+
if (field.editor === null) {
|
|
628
|
+
// Explicitly remove the editor configuration
|
|
629
|
+
delete rawEl.contentTypeElementConfiguration;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
editorFieldsToSync.push({ field, rawEl });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Resolve HTML editor names to IDs for any fields that need syncing
|
|
638
|
+
if (editorFieldsToSync.length > 0) {
|
|
639
|
+
const editors = await this._httpClient.request({
|
|
640
|
+
method: 'GET',
|
|
641
|
+
path: '/htmlEditor',
|
|
642
|
+
});
|
|
643
|
+
for (const { field, rawEl } of editorFieldsToSync) {
|
|
644
|
+
const match = editors.find((e) => e.name.toLowerCase() === field.editor.toLowerCase());
|
|
645
|
+
if (!match) {
|
|
646
|
+
const valid = editors.map((e) => `"${e.name}"`).join(', ');
|
|
647
|
+
throw new Error(`Unknown editor "${field.editor}". Valid options: ${valid}`);
|
|
648
|
+
}
|
|
649
|
+
rawEl.contentTypeElementConfiguration = { editorID: match.id };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const updated = {
|
|
653
|
+
...this._rawData,
|
|
654
|
+
alias: this.name,
|
|
655
|
+
name: this.name,
|
|
656
|
+
description: this.description,
|
|
657
|
+
minAuthLevel: authLevel,
|
|
658
|
+
workflow: String(this.workflow),
|
|
659
|
+
enableDirectEdit: this.directEdit,
|
|
660
|
+
sharedGroups: this.sharedGroups.map((id) => ({ id })),
|
|
661
|
+
primaryGroup: { id: this.primaryGroup || null },
|
|
662
|
+
contentTypeElements: rawElements,
|
|
663
|
+
};
|
|
664
|
+
// Resolve useAsFilename → elementIdforFilename
|
|
665
|
+
const filenameFields = Object.values(this.fields).filter((f) => f.useAsFilename);
|
|
666
|
+
if (filenameFields.length > 0) {
|
|
667
|
+
// Last one wins — use the last field marked as useAsFilename
|
|
668
|
+
const chosen = filenameFields[filenameFields.length - 1];
|
|
669
|
+
if (chosen.type !== 'Plain Text') {
|
|
670
|
+
throw new Error(`Only Plain Text elements can be used as filename. "${chosen.name}" is type "${chosen.type}"`);
|
|
671
|
+
}
|
|
672
|
+
const elementId = chosen._elementId;
|
|
673
|
+
if (elementId !== undefined) {
|
|
674
|
+
updated.elementIdforFilename = elementId;
|
|
675
|
+
}
|
|
676
|
+
// Clear all others
|
|
677
|
+
for (const f of Object.values(this.fields)) {
|
|
678
|
+
f.useAsFilename = f === chosen;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
await this._httpClient.request({
|
|
682
|
+
method: 'PUT',
|
|
683
|
+
path: `/contenttype/${this.id}`,
|
|
684
|
+
body: updated,
|
|
685
|
+
});
|
|
686
|
+
// Update raw data for next save
|
|
687
|
+
this._rawData = updated;
|
|
688
|
+
this._removedElementIds.clear();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
exports.ContentType = ContentType;
|
|
692
|
+
/**
|
|
693
|
+
* Resource for content type operations.
|
|
694
|
+
* Provides listing and retrieval of content type definitions.
|
|
695
|
+
*/
|
|
696
|
+
class ContentTypeResource {
|
|
697
|
+
constructor(httpClient) {
|
|
698
|
+
this.typeMapPromise = null;
|
|
699
|
+
this.typeMapExpiresAt = 0;
|
|
700
|
+
this.typeMapEpoch = -1;
|
|
701
|
+
this.editorMapPromise = null;
|
|
702
|
+
this.editorMapExpiresAt = 0;
|
|
703
|
+
this.editorMapEpoch = -1;
|
|
704
|
+
this.httpClient = httpClient;
|
|
705
|
+
}
|
|
706
|
+
/** Fetches and caches element type ID → name map from GET /type/ */
|
|
707
|
+
getTypeMap() {
|
|
708
|
+
if (!this.typeMapPromise || Date.now() > this.typeMapExpiresAt || this.typeMapEpoch < (0, utils_js_1.getCacheEpoch)()) {
|
|
709
|
+
this.typeMapExpiresAt = Date.now() + utils_js_1.DEFAULT_CACHE_TTL;
|
|
710
|
+
this.typeMapEpoch = (0, utils_js_1.getCacheEpoch)();
|
|
711
|
+
this.typeMapPromise = this.httpClient.request({
|
|
712
|
+
method: 'GET',
|
|
713
|
+
path: '/type/',
|
|
714
|
+
}).then((types) => new Map(types.map((t) => [t.id, t.name])));
|
|
715
|
+
}
|
|
716
|
+
return this.typeMapPromise;
|
|
717
|
+
}
|
|
718
|
+
/** Fetches and caches HTML editor ID → name map from GET /htmlEditor */
|
|
719
|
+
getEditorMap() {
|
|
720
|
+
if (!this.editorMapPromise || Date.now() > this.editorMapExpiresAt || this.editorMapEpoch < (0, utils_js_1.getCacheEpoch)()) {
|
|
721
|
+
this.editorMapExpiresAt = Date.now() + utils_js_1.DEFAULT_CACHE_TTL;
|
|
722
|
+
this.editorMapEpoch = (0, utils_js_1.getCacheEpoch)();
|
|
723
|
+
this.editorMapPromise = this.httpClient.request({
|
|
724
|
+
method: 'GET',
|
|
725
|
+
path: '/htmlEditor',
|
|
726
|
+
}).then((editors) => new Map(editors.map((e) => [e.id, e.name])), () => new Map());
|
|
727
|
+
}
|
|
728
|
+
return this.editorMapPromise;
|
|
729
|
+
}
|
|
730
|
+
/** Resolves listName for all fields that have a non-zero listId. */
|
|
731
|
+
async resolveListNames(contentTypes) {
|
|
732
|
+
// Collect unique list IDs across all content types
|
|
733
|
+
const listIds = new Set();
|
|
734
|
+
for (const ct of contentTypes) {
|
|
735
|
+
for (const f of Object.values(ct.fields)) {
|
|
736
|
+
if (f.listId)
|
|
737
|
+
listIds.add(f.listId);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (listIds.size === 0)
|
|
741
|
+
return;
|
|
742
|
+
// Fetch each list and build a name map
|
|
743
|
+
const listNameMap = new Map();
|
|
744
|
+
await Promise.all(Array.from(listIds).map(async (id) => {
|
|
745
|
+
try {
|
|
746
|
+
const list = await this.httpClient.request({
|
|
747
|
+
method: 'GET',
|
|
748
|
+
path: `/list/${id}/en?override=false`,
|
|
749
|
+
});
|
|
750
|
+
listNameMap.set(id, list.name ?? '');
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
(0, utils_js_1.debugWarn)(`Failed to resolve list name for listId ${id}`, error);
|
|
754
|
+
}
|
|
755
|
+
}));
|
|
756
|
+
// Apply names to fields
|
|
757
|
+
for (const ct of contentTypes) {
|
|
758
|
+
for (const f of Object.values(ct.fields)) {
|
|
759
|
+
if (f.listId) {
|
|
760
|
+
f.listName = listNameMap.get(f.listId) ?? '';
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Lists all content type definitions.
|
|
767
|
+
*/
|
|
768
|
+
async list() {
|
|
769
|
+
const [raw, typeMap, editorMap] = await Promise.all([
|
|
770
|
+
this.httpClient.request({ method: 'GET', path: '/contenttype?excludeElements=true' }),
|
|
771
|
+
this.getTypeMap(),
|
|
772
|
+
this.getEditorMap(),
|
|
773
|
+
]);
|
|
774
|
+
const results = raw.map((r) => mapContentType(r, typeMap, editorMap));
|
|
775
|
+
const dataOnly = results.map((r) => r.data);
|
|
776
|
+
await this.resolveListNames(dataOnly);
|
|
777
|
+
return results.map((r) => new ContentType(r.data, this.httpClient, r.rawData));
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Retrieves a single content type definition by ID.
|
|
781
|
+
*/
|
|
782
|
+
async get(id) {
|
|
783
|
+
const [raw, typeMap, editorMap] = await Promise.all([
|
|
784
|
+
this.httpClient.request({ method: 'GET', path: `/contenttype/${id}` }),
|
|
785
|
+
this.getTypeMap(),
|
|
786
|
+
this.getEditorMap(),
|
|
787
|
+
]);
|
|
788
|
+
const result = mapContentType(raw, typeMap, editorMap);
|
|
789
|
+
await this.resolveListNames([result.data]);
|
|
790
|
+
return new ContentType(result.data, this.httpClient, result.rawData);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Updates a content type's properties (immutable pattern).
|
|
794
|
+
* Fetches existing, merges changes, PUTs full body.
|
|
795
|
+
*/
|
|
796
|
+
async update(id, data) {
|
|
797
|
+
const ct = await this.get(id);
|
|
798
|
+
if (data.name !== undefined)
|
|
799
|
+
ct.name = data.name;
|
|
800
|
+
if (data.description !== undefined)
|
|
801
|
+
ct.description = data.description;
|
|
802
|
+
if (data.minUserLevel !== undefined)
|
|
803
|
+
ct.minUserLevel = data.minUserLevel;
|
|
804
|
+
if (data.workflow !== undefined)
|
|
805
|
+
ct.workflow = data.workflow;
|
|
806
|
+
if (data.directEdit !== undefined)
|
|
807
|
+
ct.directEdit = data.directEdit;
|
|
808
|
+
if (data.sharedGroups !== undefined)
|
|
809
|
+
ct.sharedGroups = data.sharedGroups;
|
|
810
|
+
if (data.primaryGroup !== undefined)
|
|
811
|
+
ct.primaryGroup = data.primaryGroup;
|
|
812
|
+
if (data.removeFields) {
|
|
813
|
+
for (const fieldName of data.removeFields) {
|
|
814
|
+
ct.removeField(fieldName);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (data.addFields) {
|
|
818
|
+
for (const field of data.addFields) {
|
|
819
|
+
await ct.addField(field);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
await ct.save();
|
|
823
|
+
return ct;
|
|
824
|
+
}
|
|
825
|
+
/** Deletes a content type by ID. */
|
|
826
|
+
async delete(id) {
|
|
827
|
+
await this.httpClient.request({
|
|
828
|
+
method: 'DELETE',
|
|
829
|
+
path: `/contenttype/${id}`,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
/** Creates a new content type. */
|
|
833
|
+
async create(data) {
|
|
834
|
+
if (!data.name?.trim()) {
|
|
835
|
+
throw new Error('Content type name is required');
|
|
836
|
+
}
|
|
837
|
+
if (!data.elements?.length) {
|
|
838
|
+
throw new Error('Content type must have at least one element');
|
|
839
|
+
}
|
|
840
|
+
// Validate useAsFilename constraints
|
|
841
|
+
const filenameElements = data.elements.filter((el) => el.useAsFilename);
|
|
842
|
+
if (filenameElements.length > 1) {
|
|
843
|
+
throw new Error('Only one element can be used as filename');
|
|
844
|
+
}
|
|
845
|
+
if (filenameElements.length === 1 && filenameElements[0].type.toLowerCase() !== 'plain text') {
|
|
846
|
+
throw new Error(`Only Plain Text elements can be used as filename. "${filenameElements[0].name}" is type "${filenameElements[0].type}"`);
|
|
847
|
+
}
|
|
848
|
+
const typeMap = await this.getTypeMap();
|
|
849
|
+
const reverseTypeMap = new Map(Array.from(typeMap.entries()).map(([id, name]) => [name.toLowerCase(), id]));
|
|
850
|
+
// Fetch raw types to get listType flag for validation
|
|
851
|
+
const rawTypes = await this.httpClient.request({
|
|
852
|
+
method: 'GET',
|
|
853
|
+
path: '/type/',
|
|
854
|
+
});
|
|
855
|
+
const listTypeNames = new Set(rawTypes.filter((t) => t.listType).map((t) => t.name.toLowerCase()));
|
|
856
|
+
const plainTextId = reverseTypeMap.get('plain text') ?? 1;
|
|
857
|
+
// Build elements array — Name element is always first
|
|
858
|
+
const contentTypeElements = [
|
|
859
|
+
{
|
|
860
|
+
id: 1,
|
|
861
|
+
name: 'Name',
|
|
862
|
+
description: '',
|
|
863
|
+
maxSize: 80,
|
|
864
|
+
compulsory: true,
|
|
865
|
+
type: plainTextId,
|
|
866
|
+
listId: '',
|
|
867
|
+
alias: 'Name',
|
|
868
|
+
shown: true,
|
|
869
|
+
sequence: 1,
|
|
870
|
+
},
|
|
871
|
+
];
|
|
872
|
+
// Default: Name element is used for filename
|
|
873
|
+
let elementIdForFilename = 1;
|
|
874
|
+
let editorMapForCreate = null;
|
|
875
|
+
for (let i = 0; i < data.elements.length; i++) {
|
|
876
|
+
const el = data.elements[i];
|
|
877
|
+
const typeId = reverseTypeMap.get(el.type.toLowerCase());
|
|
878
|
+
if (typeId === undefined) {
|
|
879
|
+
const valid = Array.from(typeMap.values()).join(', ');
|
|
880
|
+
throw new Error(`Unknown element type "${el.type}". Valid options: ${valid}`);
|
|
881
|
+
}
|
|
882
|
+
// Validate listId for list-based types
|
|
883
|
+
if (listTypeNames.has(el.type.toLowerCase()) && !el.listId) {
|
|
884
|
+
throw new Error(`Element "${el.name}" is type "${el.type}" which requires a listId`);
|
|
885
|
+
}
|
|
886
|
+
// Validate repeater config
|
|
887
|
+
const isRepeater = el.type.toLowerCase() === 'repeater';
|
|
888
|
+
if (isRepeater && !el.repeater) {
|
|
889
|
+
throw new Error(`Element "${el.name}" is type "Repeater" which requires a repeater configuration (contentTypeId)`);
|
|
890
|
+
}
|
|
891
|
+
if (!isRepeater && el.repeater) {
|
|
892
|
+
throw new Error(`Element "${el.name}" is type "${el.type}" — repeater configuration is only valid for Repeater elements`);
|
|
893
|
+
}
|
|
894
|
+
// Validate editor option (only valid for HTML elements)
|
|
895
|
+
const isHtml = el.type.toLowerCase() === 'html';
|
|
896
|
+
if (el.editor !== undefined && !isHtml) {
|
|
897
|
+
throw new Error(`Element "${el.name}" is type "${el.type}" — editor is only valid for HTML elements`);
|
|
898
|
+
}
|
|
899
|
+
const elId = -Math.floor(Math.random() * 1000000);
|
|
900
|
+
const elDef = {
|
|
901
|
+
id: elId,
|
|
902
|
+
name: el.name,
|
|
903
|
+
description: el.description ?? '',
|
|
904
|
+
maxSize: String(el.maxSize ?? 80),
|
|
905
|
+
compulsory: el.required ?? false,
|
|
906
|
+
type: String(typeId),
|
|
907
|
+
listId: el.listId ? String(el.listId) : '',
|
|
908
|
+
shown: el.shown ?? true,
|
|
909
|
+
sequence: i + 2,
|
|
910
|
+
};
|
|
911
|
+
// Add repeater configuration if applicable
|
|
912
|
+
if (isRepeater && el.repeater) {
|
|
913
|
+
const maxRepeats = el.repeater.maxRepeats ?? 100;
|
|
914
|
+
elDef.contentTypeElementConfiguration = {
|
|
915
|
+
contentTypeId: el.repeater.contentTypeId,
|
|
916
|
+
layout: el.repeater.layout ?? '',
|
|
917
|
+
minRepeats: el.repeater.minRepeats ?? 0,
|
|
918
|
+
maxRepeats: maxRepeats,
|
|
919
|
+
maxRepeatsLimit: maxRepeats,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
// Add HTML editor configuration if applicable
|
|
923
|
+
if (isHtml && el.editor) {
|
|
924
|
+
if (!editorMapForCreate) {
|
|
925
|
+
const editors = await this.httpClient.request({
|
|
926
|
+
method: 'GET',
|
|
927
|
+
path: '/htmlEditor',
|
|
928
|
+
});
|
|
929
|
+
editorMapForCreate = new Map(editors.map((e) => [e.name.toLowerCase(), e]));
|
|
930
|
+
}
|
|
931
|
+
const match = editorMapForCreate.get(el.editor.toLowerCase());
|
|
932
|
+
if (!match) {
|
|
933
|
+
const valid = Array.from(editorMapForCreate.values()).map((e) => `"${e.name}"`).join(', ');
|
|
934
|
+
throw new Error(`Unknown editor "${el.editor}". Valid options: ${valid}`);
|
|
935
|
+
}
|
|
936
|
+
elDef.contentTypeElementConfiguration = { editorID: match.id };
|
|
937
|
+
}
|
|
938
|
+
contentTypeElements.push(elDef);
|
|
939
|
+
if (el.useAsFilename) {
|
|
940
|
+
elementIdForFilename = elId;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const raw = await this.httpClient.request({
|
|
944
|
+
method: 'POST',
|
|
945
|
+
path: '/contenttype',
|
|
946
|
+
body: {
|
|
947
|
+
name: data.name,
|
|
948
|
+
description: data.description ?? '',
|
|
949
|
+
type: 10,
|
|
950
|
+
editable: true,
|
|
951
|
+
fullAccess: true,
|
|
952
|
+
enableDirectEdit: data.directEdit ?? true,
|
|
953
|
+
minAuthLevel: String(utils_js_1.AUTH_LEVEL_REVERSE[data.minUserLevel ?? 'contributor'] ?? 2),
|
|
954
|
+
workflow: data.workflow !== undefined ? String(data.workflow) : '',
|
|
955
|
+
warningMessage: '',
|
|
956
|
+
elementIdforFilename: elementIdForFilename,
|
|
957
|
+
conditionals: [],
|
|
958
|
+
sharedGroups: (data.sharedGroups ?? []).map((id) => ({ id })),
|
|
959
|
+
primaryGroup: { id: data.primaryGroup ?? 0 },
|
|
960
|
+
contentTypeElements,
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
const editorMap = await this.getEditorMap();
|
|
964
|
+
const result = mapContentType(raw, typeMap, editorMap);
|
|
965
|
+
await this.resolveListNames([result.data]);
|
|
966
|
+
return new ContentType(result.data, this.httpClient, result.rawData);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
exports.ContentTypeResource = ContentTypeResource;
|
|
970
|
+
//# sourceMappingURL=content-type-resource.js.map
|