datocms-plugin-sdk
Version:
309 lines (292 loc) • 11.9 kB
text/typescript
import connectToParent from 'penpal/lib/connectToParent';
import type { AssetSourcesHook } from './hooks/assetSources';
import type { BuildItemPresentationInfoHook } from './hooks/buildItemPresentationInfo';
import type { ContentAreaSidebarItemsHook } from './hooks/contentAreaSidebarItems';
import type { CustomBlockStylesForStructuredTextFieldHook } from './hooks/customBlockStylesForStructuredTextField';
import type { CustomMarksForStructuredTextFieldHook } from './hooks/customMarksForStructuredTextField';
import type { ExecuteFieldDropdownActionHook } from './hooks/executeFieldDropdownAction';
import type { ExecuteItemFormDropdownActionHook } from './hooks/executeItemFormDropdownAction';
import type { ExecuteItemsDropdownActionHook } from './hooks/executeItemsDropdownAction';
import type { ExecuteSchemaItemTypeDropdownActionHook } from './hooks/executeSchemaItemTypeDropdownAction';
import type { ExecuteUploadsDropdownActionHook } from './hooks/executeUploadsDropdownAction';
import type { FieldDropdownActionsHook } from './hooks/fieldDropdownActions';
import type { InitialLocationQueryForItemSelectorHook } from './hooks/initialLocationQueryForItemSelector';
import type { ItemCollectionOutletsHook } from './hooks/itemCollectionOutlets';
import type { ItemFormDropdownActionsHook } from './hooks/itemFormDropdownActions';
import type { ItemFormOutletsHook } from './hooks/itemFormOutlets';
import type { ItemFormSidebarPanelsHook } from './hooks/itemFormSidebarPanels';
import type { ItemFormSidebarsHook } from './hooks/itemFormSidebars';
import type { ItemsDropdownActionsHook } from './hooks/itemsDropdownActions';
import type { MainNavigationTabsHook } from './hooks/mainNavigationTabs';
import type { ManualFieldExtensionsHook } from './hooks/manualFieldExtensions';
import type { OnBeforeItemUpsertHook } from './hooks/onBeforeItemUpsert';
import type { OnBeforeItemsDestroyHook } from './hooks/onBeforeItemsDestroy';
import type { OnBeforeItemsPublishHook } from './hooks/onBeforeItemsPublish';
import type { OnBeforeItemsUnpublishHook } from './hooks/onBeforeItemsUnpublish';
import type { OnBootHook } from './hooks/onBoot';
import type { OverrideFieldExtensionsHook } from './hooks/overrideFieldExtensions';
import {
RenderAssetSourceHook,
renderAssetSourceBootstrapper,
} from './hooks/renderAssetSource';
import {
RenderConfigScreenHook,
renderConfigScreenBootstrapper,
} from './hooks/renderConfigScreen';
import {
RenderFieldExtensionHook,
renderFieldExtensionBootstrapper,
} from './hooks/renderFieldExtension';
import {
RenderInspectorHook,
renderInspectorBootstrapper,
} from './hooks/renderInspector';
import {
RenderInspectorPanelHook,
renderInspectorPanelBootstrapper,
} from './hooks/renderInspectorPanel';
import {
RenderItemCollectionOutletHook,
renderItemCollectionOutletBootstrapper,
} from './hooks/renderItemCollectionOutlet';
import {
RenderItemFormOutletHook,
renderItemFormOutletBootstrapper,
} from './hooks/renderItemFormOutlet';
import {
RenderItemFormSidebarHook,
renderItemFormSidebarBootstrapper,
} from './hooks/renderItemFormSidebar';
import {
RenderItemFormSidebarPanelHook,
renderItemFormSidebarPanelBootstrapper,
} from './hooks/renderItemFormSidebarPanel';
import {
RenderManualFieldExtensionConfigScreenHook,
renderManualFieldExtensionConfigScreenBootstrapper,
} from './hooks/renderManualFieldExtensionConfigScreen';
import { RenderModalHook, renderModalBootstrapper } from './hooks/renderModal';
import { RenderPageHook, renderPageBootstrapper } from './hooks/renderPage';
import {
RenderUploadSidebarHook,
renderUploadSidebarBootstrapper,
} from './hooks/renderUploadSidebar';
import {
RenderUploadSidebarPanelHook,
renderUploadSidebarPanelBootstrapper,
} from './hooks/renderUploadSidebarPanel';
import type { SchemaItemTypeDropdownActionsHook } from './hooks/schemaItemTypeDropdownActions';
import type { SettingsAreaSidebarItemGroupsHook } from './hooks/settingsAreaSidebarItemGroups';
import { UploadSidebarPanelsHook } from './hooks/uploadSidebarPanels';
import { UploadSidebarsHook } from './hooks/uploadSidebars';
import type { UploadsDropdownActionsHook } from './hooks/uploadsDropdownActions';
import type { ValidateManualFieldExtensionParametersHook } from './hooks/validateManualFieldExtensionParameters';
import {
Bootstrapper,
ExtractRenderHooks,
fromOneFieldIntoMultipleAndResultsById,
omit,
} from './utils';
/** The full options you can pass to the `connect` function */
export type FullConnectParameters = AssetSourcesHook &
BuildItemPresentationInfoHook &
ContentAreaSidebarItemsHook &
CustomBlockStylesForStructuredTextFieldHook &
CustomMarksForStructuredTextFieldHook &
ExecuteFieldDropdownActionHook &
ExecuteItemFormDropdownActionHook &
ExecuteItemsDropdownActionHook &
ExecuteSchemaItemTypeDropdownActionHook &
ExecuteUploadsDropdownActionHook &
FieldDropdownActionsHook &
InitialLocationQueryForItemSelectorHook &
ItemCollectionOutletsHook &
ItemFormDropdownActionsHook &
ItemFormOutletsHook &
ItemFormSidebarPanelsHook &
ItemFormSidebarsHook &
ItemsDropdownActionsHook &
MainNavigationTabsHook &
ManualFieldExtensionsHook &
OnBeforeItemsDestroyHook &
OnBeforeItemsPublishHook &
OnBeforeItemsUnpublishHook &
OnBeforeItemUpsertHook &
OnBootHook &
OverrideFieldExtensionsHook &
RenderAssetSourceHook &
RenderConfigScreenHook &
RenderFieldExtensionHook &
RenderItemCollectionOutletHook &
RenderItemFormOutletHook &
RenderItemFormSidebarHook &
RenderItemFormSidebarPanelHook &
RenderManualFieldExtensionConfigScreenHook &
RenderModalHook &
RenderPageHook &
RenderInspectorHook &
RenderInspectorPanelHook &
RenderUploadSidebarHook &
RenderUploadSidebarPanelHook &
SchemaItemTypeDropdownActionsHook &
SettingsAreaSidebarItemGroupsHook &
UploadsDropdownActionsHook &
UploadSidebarPanelsHook &
UploadSidebarsHook &
ValidateManualFieldExtensionParametersHook;
function applyColorScheme(properties: unknown): void {
if (typeof document === 'undefined') return;
const next = (properties as { colorScheme?: 'light' | 'dark' } | null)
?.colorScheme;
if (next !== 'light' && next !== 'dark') return;
if (document.documentElement.dataset.colorScheme === next) return;
document.documentElement.dataset.colorScheme = next;
// Also set the actual `color-scheme` CSS property on the root so that
// `light-dark()` resolves to the correct branch and native form controls /
// scrollbars match — everywhere in the plugin frame, including DOM rendered
// outside any `Canvas`/portal. The `data-color-scheme` attribute above is
// just a hook for explicit CSS branching; it doesn't drive `light-dark()`.
document.documentElement.style.colorScheme = next;
}
export async function connect(
rawConfiguration: Partial<FullConnectParameters> = {},
): Promise<void> {
let onChangeListener: ((newSettings: any) => void) | null = null;
let callMethodMergingBootCtxExecutor:
| ((
methodName: string,
methodArgs: unknown[],
extraCtxProperties: Record<string, unknown>,
extraCtxMethodKeys: string[],
methodCallId: string,
) => void)
| null = null;
const configuration = {
...rawConfiguration,
overrideFieldExtensions: fromOneFieldIntoMultipleAndResultsById(
rawConfiguration.overrideFieldExtensions,
),
customMarksForStructuredTextField: fromOneFieldIntoMultipleAndResultsById(
rawConfiguration.customMarksForStructuredTextField,
),
customBlockStylesForStructuredTextField:
fromOneFieldIntoMultipleAndResultsById(
rawConfiguration.customBlockStylesForStructuredTextField,
),
};
const penpalConnection = connectToParent({
methods: {
sdkVersion: () => '0.3.1',
implementedHooks: () =>
Object.fromEntries(
Object.keys(rawConfiguration).map((hook) => {
return [hook, true];
}),
),
// What hooks should we expose via penpal as direct callable methods by CMS?
// * all renderXXX hooks will be called via onChange() -> bootstrapper, so not needed
// * all non-render hooks ending with ctx will be called via callMethodMergingBootCtx(), so not needed
// * only the non-render hooks NOT ending with ctx need to be directly called by the CMS!
// In the following lines we're exposing more than needed (all non-render hooks).. but it's OK.
...Object.fromEntries(
Object.entries(configuration).filter(
([key]) => !key.startsWith('render'),
),
),
onChange(newSettings: unknown) {
applyColorScheme(newSettings);
if (onChangeListener) {
onChangeListener(newSettings);
}
},
callMethodMergingBootCtx(
methodName: string,
methodArgs: unknown[],
extraCtxProperties: Record<string, unknown>,
extraCtxMethodKeys: string[],
methodCallId: string,
) {
if (!callMethodMergingBootCtxExecutor) {
return null;
}
return callMethodMergingBootCtxExecutor(
methodName,
methodArgs,
extraCtxProperties,
extraCtxMethodKeys,
methodCallId,
);
},
},
});
const methods = await penpalConnection.promise;
const initialProperties = await methods.getSettings();
applyColorScheme(initialProperties);
if (initialProperties.mode === 'onBoot') {
let currentProperties = initialProperties;
onChangeListener = (newProperties) => {
currentProperties = newProperties;
};
callMethodMergingBootCtxExecutor = (
methodName: string,
methodArgs: unknown[],
extraCtxProperties: Record<string, unknown>,
extraCtxMethodKeys: string[],
methodCallId: string,
) => {
if (!(methodName in configuration)) {
return undefined;
}
return (configuration as any)[methodName](...methodArgs, {
...omit(methods, ['getSettings', 'setHeight']),
...omit(currentProperties, ['mode', 'bodyPadding']),
...Object.fromEntries(
extraCtxMethodKeys.map((methodName) => [
methodName,
function createAdditionalMethodProxy(...args: any[]) {
return (methods as any).callAdditionalCtxMethod(
methodCallId,
methodName,
args,
);
},
]),
),
...extraCtxProperties,
});
};
if (configuration.onBoot) {
configuration.onBoot({
...methods,
...currentProperties,
});
}
}
type EnsureAllBootstrappers = {
[K in keyof ExtractRenderHooks<FullConnectParameters>]: Bootstrapper<K>;
};
const availableBootstrappers: EnsureAllBootstrappers = {
renderAssetSource: renderAssetSourceBootstrapper,
renderConfigScreen: renderConfigScreenBootstrapper,
renderFieldExtension: renderFieldExtensionBootstrapper,
renderItemCollectionOutlet: renderItemCollectionOutletBootstrapper,
renderItemFormOutlet: renderItemFormOutletBootstrapper,
renderItemFormSidebar: renderItemFormSidebarBootstrapper,
renderItemFormSidebarPanel: renderItemFormSidebarPanelBootstrapper,
renderManualFieldExtensionConfigScreen:
renderManualFieldExtensionConfigScreenBootstrapper,
renderModal: renderModalBootstrapper,
renderPage: renderPageBootstrapper,
renderInspector: renderInspectorBootstrapper,
renderInspectorPanel: renderInspectorPanelBootstrapper,
renderUploadSidebar: renderUploadSidebarBootstrapper,
renderUploadSidebarPanel: renderUploadSidebarPanelBootstrapper,
};
for (const bootstrapper of Object.values(availableBootstrappers)) {
const result = bootstrapper(configuration, methods, initialProperties);
if (result) {
onChangeListener = result;
break;
}
}
}