datocms-plugin-sdk 0.3.24-alpha.3 → 0.3.27

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/src/connect.ts ADDED
@@ -0,0 +1,527 @@
1
+ import connectToParent from 'penpal/lib/connectToParent';
2
+ import { Field, ModelBlock } from './SiteApiSchema';
3
+ import {
4
+ AssetSource,
5
+ ContentAreaSidebarItem,
6
+ FieldExtensionOverride,
7
+ InitPropertiesAndMethods,
8
+ ItemFormSidebarPanel,
9
+ MainNavigationTab,
10
+ ManualFieldExtension,
11
+ OnBootMethods,
12
+ OnBootPropertiesAndMethods,
13
+ RenderAssetSourceMethods,
14
+ RenderAssetSourcePropertiesAndMethods,
15
+ RenderConfigScreenMethods,
16
+ RenderConfigScreenPropertiesAndMethods,
17
+ RenderFieldExtensionMethods,
18
+ RenderFieldExtensionPropertiesAndMethods,
19
+ RenderManualFieldExtensionConfigScreenMethods,
20
+ RenderManualFieldExtensionConfigScreenPropertiesAndMethods,
21
+ RenderModalMethods,
22
+ RenderModalPropertiesAndMethods,
23
+ RenderPageMethods,
24
+ RenderPagePropertiesAndMethods,
25
+ RenderSidebarPanelMethods,
26
+ RenderSidebarPanePropertiesAndMethods,
27
+ SettingsAreaSidebarItemGroup,
28
+ } from './types';
29
+ import {
30
+ isInitParent,
31
+ isOnBootParent,
32
+ isRenderAssetSourceParent,
33
+ isRenderConfigScreenParent,
34
+ isRenderFieldExtensionParent,
35
+ isRenderManualFieldExtensionConfigScreenParent,
36
+ isRenderModalParent,
37
+ isRenderPageParent,
38
+ isRenderSidebarPaneParent,
39
+ Parent,
40
+ } from './guards';
41
+
42
+ export type SizingUtilities = {
43
+ /**
44
+ * Listens for DOM changes and automatically calls `setHeight` when it detects
45
+ * a change. If you're using `datocms-react-ui` package, the `<Canvas />`
46
+ * component already takes care of calling this method for you.
47
+ */
48
+ startAutoResizer: () => void;
49
+ /** Stops resizing the iframe automatically */
50
+ stopAutoResizer: () => void;
51
+ /**
52
+ * Triggers a change in the size of the iframe. If you don't explicitely pass
53
+ * a `newHeight` it will be automatically calculated using the iframe content
54
+ * at the moment
55
+ */
56
+ updateHeight: (newHeight?: number) => void;
57
+ };
58
+
59
+ export type { Field, ModelBlock };
60
+
61
+ export type IntentCtx = InitPropertiesAndMethods;
62
+ export type OnBootCtx = OnBootPropertiesAndMethods;
63
+ export type FieldIntentCtx = InitPropertiesAndMethods & {
64
+ itemType: ModelBlock;
65
+ };
66
+ export type RenderPageCtx = RenderPagePropertiesAndMethods;
67
+ export type RenderModalCtx = RenderModalPropertiesAndMethods & SizingUtilities;
68
+ export type RenderAssetSourceCtx = RenderAssetSourcePropertiesAndMethods &
69
+ SizingUtilities;
70
+ export type RenderItemFormSidebarPanelCtx = RenderSidebarPanePropertiesAndMethods &
71
+ SizingUtilities;
72
+ export type RenderFieldExtensionCtx = RenderFieldExtensionPropertiesAndMethods &
73
+ SizingUtilities;
74
+ export type RenderManualFieldExtensionConfigScreenCtx = RenderManualFieldExtensionConfigScreenPropertiesAndMethods &
75
+ SizingUtilities;
76
+ export type RenderConfigScreenCtx = RenderConfigScreenPropertiesAndMethods &
77
+ SizingUtilities;
78
+
79
+ /** The full options you can pass to the `connect` function */
80
+ export type FullConnectParameters = {
81
+ /**
82
+ * This function will be called once at boot time and can be used to perform
83
+ * ie. some initial integrity checks on the configuration.
84
+ *
85
+ * @group boot
86
+ */
87
+ onBoot: (ctx: OnBootCtx) => void;
88
+ /**
89
+ * Use this function to declare new tabs you want to add in the top-bar of the UI
90
+ *
91
+ * @group pages
92
+ */
93
+ mainNavigationTabs: (ctx: IntentCtx) => MainNavigationTab[];
94
+ /**
95
+ * Use this function to declare new navigation sections in the Settings Area sidebar
96
+ *
97
+ * @group pages
98
+ */
99
+ settingsAreaSidebarItemGroups: (
100
+ ctx: IntentCtx,
101
+ ) => SettingsAreaSidebarItemGroup[];
102
+ /**
103
+ * Use this function to declare new navigation items in the Content Area sidebar
104
+ *
105
+ * @group pages
106
+ */
107
+ contentAreaSidebarItems: (ctx: IntentCtx) => ContentAreaSidebarItem[];
108
+ /**
109
+ * Use this function to declare new field extensions that users will be able
110
+ * to install manually in some field
111
+ *
112
+ * @group manualFieldExtensions
113
+ */
114
+ manualFieldExtensions: (ctx: IntentCtx) => ManualFieldExtension[];
115
+ /**
116
+ * Use this function to declare additional sources to be shown when users want
117
+ * to upload new assets
118
+ *
119
+ * @group assetSources
120
+ */
121
+ assetSources: (ctx: IntentCtx) => AssetSource[] | void;
122
+ /**
123
+ * Use this function to declare new sidebar panels to be shown when the user
124
+ * edits records of a particular model
125
+ *
126
+ * @group sidebarPanels
127
+ */
128
+ itemFormSidebarPanels: (
129
+ itemType: ModelBlock,
130
+ ctx: IntentCtx,
131
+ ) => ItemFormSidebarPanel[];
132
+ /**
133
+ * Use this function to automatically force one or more field extensions to a
134
+ * particular field
135
+ *
136
+ * @group forcedFieldExtensions
137
+ */
138
+ overrideFieldExtensions: (
139
+ field: Field,
140
+ ctx: FieldIntentCtx,
141
+ ) => FieldExtensionOverride | void;
142
+ /**
143
+ * This function will be called when the plugin needs to render the plugin's
144
+ * configuration form
145
+ *
146
+ * @group configScreen
147
+ */
148
+ renderConfigScreen: (ctx: RenderConfigScreenCtx) => void;
149
+ /**
150
+ * This function will be called when the plugin needs to render a specific
151
+ * page (see the `mainNavigationTabs`, `settingsAreaSidebarItemGroups` and
152
+ * `contentAreaSidebarItems` functions)
153
+ *
154
+ * @group pages
155
+ */
156
+ renderPage: (pageId: string, ctx: RenderPageCtx) => void;
157
+ /**
158
+ * This function will be called when the plugin requested to open a modal (see
159
+ * the `openModal` function)
160
+ *
161
+ * @group modals
162
+ */
163
+ renderModal: (modalId: string, ctx: RenderModalCtx) => void;
164
+ /**
165
+ * This function will be called when the plugin needs to render a sidebar
166
+ * panel (see the `itemFormSidebarPanels` function)
167
+ *
168
+ * @group sidebarPanels
169
+ */
170
+ renderItemFormSidebarPanel: (
171
+ sidebarPaneId: string,
172
+ ctx: RenderItemFormSidebarPanelCtx,
173
+ ) => void;
174
+ /**
175
+ * This function will be called when the user selects one of the plugin's
176
+ * asset sources to upload a new media file.
177
+ *
178
+ * @group assetSources
179
+ */
180
+ renderAssetSource: (assetSourceId: string, ctx: RenderAssetSourceCtx) => void;
181
+ /**
182
+ * This function will be called when the plugin needs to render a field
183
+ * extension (see the `manualFieldExtensions` and `overrideFieldExtensions` functions)
184
+ *
185
+ * @group forcedFieldExtensions
186
+ */
187
+ renderFieldExtension: (
188
+ fieldExtensionId: string,
189
+ ctx: RenderFieldExtensionCtx,
190
+ ) => void;
191
+ /**
192
+ * This function will be called when the plugin needs to render the
193
+ * configuration form for installing a field extension inside a particular field
194
+ *
195
+ * @group manualFieldExtensions
196
+ */
197
+ renderManualFieldExtensionConfigScreen: (
198
+ fieldExtensionId: string,
199
+ ctx: RenderManualFieldExtensionConfigScreenCtx,
200
+ ) => void;
201
+ /**
202
+ * This function will be called each time the configuration object changes. It
203
+ * must return an object containing possible validation errors
204
+ *
205
+ * @group manualFieldExtensions
206
+ */
207
+ validateManualFieldExtensionParameters: (
208
+ fieldExtensionId: string,
209
+ parameters: Record<string, unknown>,
210
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
211
+ };
212
+
213
+ function toMultifield<Result>(
214
+ fn: ((field: Field, ctx: FieldIntentCtx) => Result) | undefined,
215
+ ) {
216
+ return (
217
+ fields: Field[],
218
+ ctx: InitPropertiesAndMethods,
219
+ ): Record<string, Result> => {
220
+ if (!fn) {
221
+ return {};
222
+ }
223
+
224
+ const result: Record<string, Result> = {};
225
+
226
+ for (const field of fields) {
227
+ const itemType = ctx.itemTypes[
228
+ field.relationships.item_type.data.id
229
+ ] as ModelBlock;
230
+ result[field.id] = fn(field, { ...ctx, itemType });
231
+ }
232
+
233
+ return result;
234
+ };
235
+ }
236
+
237
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
+ type AsyncReturnType<T extends (...args: any) => any> = T extends (
239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
+ ...args: any
241
+ ) => Promise<infer U>
242
+ ? U
243
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
+ T extends (...args: any) => infer U
245
+ ? U
246
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
+ any;
248
+
249
+ const buildRenderUtils = (parent: { setHeight: (number: number) => void }) => {
250
+ let oldHeight: null | number = null;
251
+
252
+ const updateHeight = (height?: number) => {
253
+ const realHeight =
254
+ height === undefined
255
+ ? Math.ceil(document.documentElement.getBoundingClientRect().height)
256
+ : height;
257
+
258
+ if (realHeight !== oldHeight) {
259
+ parent.setHeight(realHeight);
260
+ oldHeight = realHeight;
261
+ }
262
+ };
263
+
264
+ let autoResizingActive = false;
265
+ let mutationObserver: MutationObserver | null = null;
266
+
267
+ const resetHeight = () => updateHeight();
268
+
269
+ const startAutoResizer = () => {
270
+ updateHeight();
271
+
272
+ if (autoResizingActive) {
273
+ return;
274
+ }
275
+
276
+ autoResizingActive = true;
277
+
278
+ mutationObserver = new MutationObserver(resetHeight);
279
+
280
+ mutationObserver.observe(window.document.body, {
281
+ attributes: true,
282
+ childList: true,
283
+ subtree: true,
284
+ characterData: true,
285
+ });
286
+
287
+ window.addEventListener('resize', resetHeight);
288
+ };
289
+
290
+ const stopAutoResizer = () => {
291
+ if (!autoResizingActive) {
292
+ return;
293
+ }
294
+
295
+ autoResizingActive = false;
296
+
297
+ if (mutationObserver) {
298
+ mutationObserver.disconnect();
299
+ }
300
+
301
+ window.removeEventListener('resize', resetHeight);
302
+ };
303
+
304
+ return { updateHeight, startAutoResizer, stopAutoResizer };
305
+ };
306
+
307
+ export async function connect(
308
+ configuration: Partial<FullConnectParameters> = {},
309
+ ): Promise<void> {
310
+ const {
311
+ assetSources,
312
+ mainNavigationTabs,
313
+ settingsAreaSidebarItemGroups,
314
+ contentAreaSidebarItems,
315
+ manualFieldExtensions,
316
+ itemFormSidebarPanels,
317
+ } = configuration;
318
+
319
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
+ let listener: ((newSettings: any) => void) | null = null;
321
+
322
+ const penpalConnection = connectToParent({
323
+ methods: {
324
+ sdkVersion: () => '0.2.0',
325
+ implementedHooks: () =>
326
+ Object.fromEntries(
327
+ Object.entries(configuration).map(([key, value]) => {
328
+ if (typeof value === 'function') {
329
+ return [key, true];
330
+ }
331
+
332
+ return [key, value];
333
+ }),
334
+ ),
335
+ assetSources,
336
+ mainNavigationTabs,
337
+ settingsAreaSidebarItemGroups,
338
+ contentAreaSidebarItems,
339
+ manualFieldExtensions,
340
+ itemFormSidebarPanels,
341
+ overrideFieldExtensions: toMultifield(
342
+ configuration.overrideFieldExtensions,
343
+ ),
344
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
345
+ onChange(newSettings: any) {
346
+ if (listener) {
347
+ listener(newSettings);
348
+ }
349
+ },
350
+ validateManualFieldExtensionParameters:
351
+ configuration.validateManualFieldExtensionParameters,
352
+ },
353
+ });
354
+
355
+ const parent: Parent = await penpalConnection.promise;
356
+ const initialSettings = await parent.getSettings();
357
+
358
+ if (isInitParent(parent, initialSettings)) {
359
+ // Nothing to do. Parent calls the method they need.
360
+ }
361
+
362
+ if (isOnBootParent(parent, initialSettings)) {
363
+ type Settings = AsyncReturnType<OnBootMethods['getSettings']>;
364
+
365
+ const render = (settings: Settings) => {
366
+ if (!configuration.onBoot) {
367
+ return;
368
+ }
369
+
370
+ configuration.onBoot({
371
+ ...parent,
372
+ ...settings,
373
+ });
374
+ };
375
+
376
+ render(initialSettings as Settings);
377
+ }
378
+
379
+ if (isRenderPageParent(parent, initialSettings)) {
380
+ type Settings = AsyncReturnType<RenderPageMethods['getSettings']>;
381
+
382
+ const render = (settings: Settings) => {
383
+ if (!configuration.renderPage) {
384
+ return;
385
+ }
386
+
387
+ configuration.renderPage(settings.pageId, {
388
+ ...parent,
389
+ ...settings,
390
+ });
391
+ };
392
+
393
+ listener = render;
394
+ render(initialSettings as Settings);
395
+ }
396
+
397
+ if (isRenderConfigScreenParent(parent, initialSettings)) {
398
+ type Settings = AsyncReturnType<RenderConfigScreenMethods['getSettings']>;
399
+
400
+ const renderUtils = buildRenderUtils(parent);
401
+
402
+ const render = (settings: Settings) => {
403
+ if (!configuration.renderConfigScreen) {
404
+ return;
405
+ }
406
+
407
+ configuration.renderConfigScreen({
408
+ ...parent,
409
+ ...settings,
410
+ ...renderUtils,
411
+ });
412
+ };
413
+
414
+ listener = render;
415
+ render(initialSettings as Settings);
416
+ }
417
+
418
+ if (isRenderModalParent(parent, initialSettings)) {
419
+ type Settings = AsyncReturnType<RenderModalMethods['getSettings']>;
420
+
421
+ const renderUtils = buildRenderUtils(parent);
422
+
423
+ const render = (settings: Settings) => {
424
+ if (!configuration.renderModal) {
425
+ return;
426
+ }
427
+
428
+ configuration.renderModal(settings.modalId, {
429
+ ...parent,
430
+ ...settings,
431
+ ...renderUtils,
432
+ });
433
+ };
434
+
435
+ listener = render;
436
+ render(initialSettings as Settings);
437
+ }
438
+
439
+ if (isRenderAssetSourceParent(parent, initialSettings)) {
440
+ type Settings = AsyncReturnType<RenderAssetSourceMethods['getSettings']>;
441
+
442
+ const renderUtils = buildRenderUtils(parent);
443
+
444
+ const render = (settings: Settings) => {
445
+ if (!configuration.renderAssetSource) {
446
+ return;
447
+ }
448
+
449
+ configuration.renderAssetSource(settings.assetSourceId, {
450
+ ...parent,
451
+ ...settings,
452
+ ...renderUtils,
453
+ });
454
+ };
455
+
456
+ listener = render;
457
+ render(initialSettings as Settings);
458
+ }
459
+
460
+ if (isRenderSidebarPaneParent(parent, initialSettings)) {
461
+ type Settings = AsyncReturnType<RenderSidebarPanelMethods['getSettings']>;
462
+
463
+ const renderUtils = buildRenderUtils(parent);
464
+
465
+ const render = (settings: Settings) => {
466
+ if (!configuration.renderItemFormSidebarPanel) {
467
+ return;
468
+ }
469
+
470
+ configuration.renderItemFormSidebarPanel(settings.sidebarPaneId, {
471
+ ...parent,
472
+ ...settings,
473
+ ...renderUtils,
474
+ });
475
+ };
476
+
477
+ listener = render;
478
+ render(initialSettings as Settings);
479
+ }
480
+
481
+ if (isRenderFieldExtensionParent(parent, initialSettings)) {
482
+ type Settings = AsyncReturnType<RenderFieldExtensionMethods['getSettings']>;
483
+
484
+ const renderUtils = buildRenderUtils(parent);
485
+
486
+ const render = (settings: Settings) => {
487
+ if (!configuration.renderFieldExtension) {
488
+ return;
489
+ }
490
+
491
+ configuration.renderFieldExtension(settings.fieldExtensionId, {
492
+ ...parent,
493
+ ...settings,
494
+ ...renderUtils,
495
+ });
496
+ };
497
+
498
+ listener = render;
499
+ render(initialSettings as Settings);
500
+ }
501
+
502
+ if (isRenderManualFieldExtensionConfigScreenParent(parent, initialSettings)) {
503
+ type Settings = AsyncReturnType<
504
+ RenderManualFieldExtensionConfigScreenMethods['getSettings']
505
+ >;
506
+
507
+ const renderUtils = buildRenderUtils(parent);
508
+
509
+ const render = (settings: Settings) => {
510
+ if (!configuration.renderManualFieldExtensionConfigScreen) {
511
+ return;
512
+ }
513
+
514
+ configuration.renderManualFieldExtensionConfigScreen(
515
+ settings.fieldExtensionId,
516
+ {
517
+ ...parent,
518
+ ...settings,
519
+ ...renderUtils,
520
+ },
521
+ );
522
+ };
523
+
524
+ listener = render;
525
+ render(initialSettings as Settings);
526
+ }
527
+ }
@@ -0,0 +1 @@
1
+ declare module 'penpal/lib/connectToParent';
package/src/guards.ts ADDED
@@ -0,0 +1,48 @@
1
+ import {
2
+ InitMethods,
3
+ OnBootMethods,
4
+ RenderPageMethods,
5
+ RenderFieldExtensionMethods,
6
+ RenderConfigScreenMethods,
7
+ RenderManualFieldExtensionConfigScreenMethods,
8
+ RenderSidebarPanelMethods,
9
+ RenderModalMethods,
10
+ RenderAssetSourceMethods,
11
+ } from './types';
12
+
13
+ export type Parent = { getSettings: () => Promise<{ mode: string }> };
14
+
15
+ function buildGuard<P extends Parent>(mode: string) {
16
+ return (parent: Parent, settings: { mode: string }): parent is P =>
17
+ settings.mode === mode;
18
+ }
19
+
20
+ export const isInitParent = buildGuard<InitMethods>('init');
21
+
22
+ export const isOnBootParent = buildGuard<OnBootMethods>('onBoot');
23
+
24
+ export const isRenderPageParent = buildGuard<RenderPageMethods>('renderPage');
25
+
26
+ export const isRenderConfigScreenParent = buildGuard<RenderConfigScreenMethods>(
27
+ 'renderConfigScreen',
28
+ );
29
+
30
+ export const isRenderModalParent = buildGuard<RenderModalMethods>(
31
+ 'renderModal',
32
+ );
33
+
34
+ export const isRenderSidebarPaneParent = buildGuard<RenderSidebarPanelMethods>(
35
+ 'renderItemFormSidebarPanel',
36
+ );
37
+
38
+ export const isRenderFieldExtensionParent = buildGuard<RenderFieldExtensionMethods>(
39
+ 'renderFieldExtension',
40
+ );
41
+
42
+ export const isRenderManualFieldExtensionConfigScreenParent = buildGuard<RenderManualFieldExtensionConfigScreenMethods>(
43
+ 'renderManualFieldExtensionConfigScreen',
44
+ );
45
+
46
+ export const isRenderAssetSourceParent = buildGuard<RenderAssetSourceMethods>(
47
+ 'renderAssetSource',
48
+ );
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ import {
2
+ Account,
3
+ Field,
4
+ Item,
5
+ ModelBlock,
6
+ Plugin,
7
+ Site,
8
+ SsoUser,
9
+ Upload,
10
+ User,
11
+ Role,
12
+ } from './SiteApiSchema';
13
+
14
+ export type {
15
+ Account,
16
+ Field,
17
+ Item,
18
+ ModelBlock,
19
+ Plugin,
20
+ Site,
21
+ SsoUser,
22
+ Upload,
23
+ User,
24
+ Role,
25
+ };
26
+
27
+ export * from './connect';
28
+ export * from './types';