diff --git a/ui/global.d.ts b/ui/global.d.ts index 331f91ce60746f0728ad5e5272f1e2300cb82505..8f1746972f12e8ff204b6bac8d4def27f0758a5c 100644 --- a/ui/global.d.ts +++ b/ui/global.d.ts @@ -1,36 +1,9 @@ -import type { PropsValues } from '@/types/Form'; import type React from 'react'; import type ReactDom from 'react-dom'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import type * as ReactRedux from 'react-redux'; import type * as ReduxToolkit from '@reduxjs/toolkit'; - -interface DrupalSettings { - xb: { - base: string; - entityType: string; - entity: string; - entityTypeKeys: { - id: string; - label: string; - }; - globalAssets: { - css: string; - jsHeader: string; - jsFooter: string; - }; - layoutUtils: PropsValues; - componentSelectionUtils: PropsValues; - navUtils: PropsValues; - xbModulePath: string; - selectedComponent: string; - devMode: boolean; - }; - xbExtension: object; - path: { - baseUrl: string; - }; -} +import type { DrupalSettings } from '@/types/DrupalSettings'; interface CKEditor5Types { editorClassic: { diff --git a/ui/src/components/ComponentPreview.tsx b/ui/src/components/ComponentPreview.tsx index 9c3d6cc36758ea846b9197cf0bed5f4a6974d1c3..21ffccbefdf1377afcfc51a39489b1d705d35c08 100644 --- a/ui/src/components/ComponentPreview.tsx +++ b/ui/src/components/ComponentPreview.tsx @@ -4,12 +4,14 @@ import styles from './ComponentPreview.module.css'; import clsx from 'clsx'; import type { XBComponent } from '@/types/Component'; import type { Section } from '@/types/Section'; +import { getBaseUrl, getDrupalSettings } from '@/utils/drupal-globals'; interface ComponentPreviewProps { componentListItem: XBComponent | Section; } -const { drupalSettings } = window; +const drupalSettings = getDrupalSettings(); +const baseUrl = getBaseUrl(); const ComponentPreview: React.FC<ComponentPreviewProps> = ({ componentListItem, @@ -27,7 +29,7 @@ const ComponentPreview: React.FC<ComponentPreviewProps> = ({ drupalSettings?.xb.globalAssets.jsHeader + component.js_header; const markup = component.default_markup; - const base_url = window.location.origin + drupalSettings?.path.baseUrl; + const base_url = window.location.origin + baseUrl; const html = ` <html> diff --git a/ui/src/components/extensions/ExtensionsList.tsx b/ui/src/components/extensions/ExtensionsList.tsx index 6ca53731e863dfd9be48316a8ee1083573351d16..6e665a763e716c6f2c3233738f5f06c52140441c 100644 --- a/ui/src/components/extensions/ExtensionsList.tsx +++ b/ui/src/components/extensions/ExtensionsList.tsx @@ -3,10 +3,17 @@ import { ExternalLinkIcon } from '@radix-ui/react-icons'; import ExtensionButton from '@/components/extensions/ExtensionButton'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import type React from 'react'; +import { + getBaseUrl, + getDrupalSettings, + getXbSettings, +} from '@/utils/drupal-globals'; interface ExtensionsPopoverProps {} -const { drupalSettings } = window; +const drupalSettings = getDrupalSettings(); +const baseUrl = getBaseUrl(); +const xbSettings = getXbSettings(); const ExtensionsList: React.FC<ExtensionsPopoverProps> = () => { let extensionsList = []; @@ -16,7 +23,7 @@ const ExtensionsList: React.FC<ExtensionsPopoverProps> = () => { ...value, imgSrc: value.imgSrc || - `${drupalSettings.path.baseUrl}${drupalSettings.xb.xbModulePath}/ui/assets/icons/extension-default-abstract.svg`, + `${baseUrl}${xbSettings.xbModulePath}/ui/assets/icons/extension-default-abstract.svg`, name: value.name, description: value.description, }; diff --git a/ui/src/components/form/components/drupal/DrupalTextArea.tsx b/ui/src/components/form/components/drupal/DrupalTextArea.tsx index 2a6b49cb197c182487298946d741b4ea0674ff44..9b02c611828d6400e9ebb892d00910986aed91bd 100644 --- a/ui/src/components/form/components/drupal/DrupalTextArea.tsx +++ b/ui/src/components/form/components/drupal/DrupalTextArea.tsx @@ -5,7 +5,9 @@ import { useRef, useState } from 'react'; import { Flex } from '@radix-ui/themes'; import type { Attributes } from '@/types/DrupalAttribute'; import DrupalFormattedTextArea from './DrupalFormattedTextArea'; -const { drupalSettings } = window as any; +import { getDrupalSettings } from '@/utils/drupal-globals'; +import type { FormatType } from '@/types/FormatType'; +const drupalSettings = getDrupalSettings(); const DrupalTextArea = ({ attributes = {}, @@ -67,20 +69,6 @@ const DrupalTextArea = ({ ); }; -interface FormatType { - format: string; - editor?: string; - editorSettings?: { - toolbar: any[]; - plugins: string[]; - config: { - [key: string]: any; - }; - language: Record<string, any>; - }; - [key: string]: any; -} - interface FormatSelectProps { attributes: Attributes; selectAttributes: Record<string, any>; diff --git a/ui/src/components/form/formUtil.ts b/ui/src/components/form/formUtil.ts index fbc40cdd079a537e7a88a9f772f8b92207a55fde..a1f6ce1958d0d13536392ccf540af46bc7b1012d 100644 --- a/ui/src/components/form/formUtil.ts +++ b/ui/src/components/form/formUtil.ts @@ -14,6 +14,7 @@ import type { TransformConfig, Transforms } from '@/utils/transforms'; import transforms from '@/utils/transforms'; import qs from 'qs'; import addDraft2019 from 'ajv-formats-draft2019'; +import { getDrupal } from '@/utils/drupal-globals'; const ajv = new Ajv(); addFormats(ajv); addDraft2019(ajv); @@ -338,7 +339,7 @@ export function getPropsValues( // Iterate through every item in form state that corresponds to // a component input to create propsValues, which will ultimately be // used to update this component's model. - const { Drupal } = (window as any) || { + const Drupal = getDrupal() || { Drupal: { xbTransforms: transforms }, }; const transformsList: Transforms = Drupal?.xbTransforms || transforms; diff --git a/ui/src/components/pageInfo/PageInfo.tsx b/ui/src/components/pageInfo/PageInfo.tsx index dbd92beef0390e4afdedbd705d9afddb2507f5b5..cf206b314f2e16223a8f7f5d50a3b69658258965 100644 --- a/ui/src/components/pageInfo/PageInfo.tsx +++ b/ui/src/components/pageInfo/PageInfo.tsx @@ -40,7 +40,7 @@ import { selectEntityId, selectEntityType, } from '@/features/configuration/configurationSlice'; -import { getBaseUrl } from '@/utils/drupal-globals'; +import { getBaseUrl, getXbSettings } from '@/utils/drupal-globals'; interface PageType { [key: string]: ReactElement; @@ -53,7 +53,7 @@ const iconMap: PageType = { GlobalSectionName: <SectionIcon />, }; -const { drupalSettings } = window; +const xbSettings = getXbSettings(); const PageInfo = () => { const { showBoundary } = useErrorBoundary(); @@ -68,7 +68,7 @@ const PageInfo = () => { )?.name; const entity_form_fields = useAppSelector(selectPageData); const title = - entity_form_fields[`${drupalSettings.xb.entityTypeKeys.label}[0][value]`]; + entity_form_fields[`${xbSettings.entityTypeKeys.label}[0][value]`]; const { data: pageItems, diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 9b928410c7f09c03c5a393e2807663fd826b419a..b3ea2282ecdb89e257ba6e93e313961fcf64433c 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -21,10 +21,11 @@ import ExtensionsList from '@/components/extensions/ExtensionsList'; import TopbarPopover from '@/components/topbar/menu/TopbarPopover'; import topBarStyles from '@/components/topbar/Topbar.module.css'; import DynamicComponents from '@/components/dynamicComponents/DynamicComponents'; +import { getDrupalSettings } from '@/utils/drupal-globals'; const PREVIOUS_URL_STORAGE_KEY = 'XBPreviousURL'; -const { drupalSettings } = window; +const drupalSettings = getDrupalSettings(); const Topbar = () => { const navigate = useNavigate(); diff --git a/ui/src/features/drupal/drupalUtil.ts b/ui/src/features/drupal/drupalUtil.ts deleted file mode 100644 index 7a66dd17f6a6463197ad1cb79fe7a897a09be9a8..0000000000000000000000000000000000000000 --- a/ui/src/features/drupal/drupalUtil.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { PropsValues } from '@/types/Form'; - -const { drupalSettings } = window; -export const setXbDrupalSetting = ( - property: 'layoutUtils' | 'navUtils', - value: PropsValues, -) => { - if (drupalSettings?.xb?.[property]) { - drupalSettings.xb[property] = { ...drupalSettings.xb[property], ...value }; - } -}; diff --git a/ui/src/features/layout/layoutModelSlice.ts b/ui/src/features/layout/layoutModelSlice.ts index a2e01b7b1c61b00b5e0c7672db79c2f2ff67dd78..dee6177f45c73d54808b52d5aee8ecf9ffcd2ffa 100644 --- a/ui/src/features/layout/layoutModelSlice.ts +++ b/ui/src/features/layout/layoutModelSlice.ts @@ -8,7 +8,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { StateWithHistory } from 'redux-undo'; import { v4 as uuidv4 } from 'uuid'; -import { setXbDrupalSetting } from '@/features/drupal/drupalUtil'; +import { setXbDrupalSetting } from '@/utils/drupal-globals'; import { findComponentByUuid, findNodePathByUuid, diff --git a/ui/src/features/layout/layoutUtils.ts b/ui/src/features/layout/layoutUtils.ts index 8c4b3baf6ebfac3dd36ec08f7b72a9ad72003267..fca9fd9e1c9011eebe9eb6a398489adbeeb33b72 100644 --- a/ui/src/features/layout/layoutUtils.ts +++ b/ui/src/features/layout/layoutUtils.ts @@ -8,7 +8,7 @@ import type { } from './layoutModelSlice'; import { NodeType } from './layoutModelSlice'; import { v4 as uuidv4 } from 'uuid'; -import { setXbDrupalSetting } from '@/features/drupal/drupalUtil'; +import { setXbDrupalSetting } from '@/utils/drupal-globals'; import { isConsecutive } from '@/utils/function-utils'; type NodeFunction = ( diff --git a/ui/src/features/layout/preview/Preview.tsx b/ui/src/features/layout/preview/Preview.tsx index 6d23b26ce27635002e47bb589d6f16db6d443930..010011d9e4b5e42067f169d3f961918e233a744d 100644 --- a/ui/src/features/layout/preview/Preview.tsx +++ b/ui/src/features/layout/preview/Preview.tsx @@ -18,6 +18,7 @@ import { selectPageData } from '@/features/pageData/pageDataSlice'; import { selectPreviewHtml } from '@/features/pagePreview/previewSlice'; import { contentApi } from '@/services/content'; import { selectSelectedComponentUuid } from '@/features/ui/uiSlice'; +import { getXbSettings } from '@/utils/drupal-globals'; interface PreviewProps {} @@ -33,9 +34,9 @@ const previewSizes = { name: 'Mobile', }, }; -const { drupalSettings } = window; +const xbSettings = getXbSettings(); type PreviewSizeKey = keyof typeof previewSizes; -const labelFormKey = `${drupalSettings.xb.entityTypeKeys.label}[0][value]`; +const labelFormKey = `${xbSettings.entityTypeKeys.label}[0][value]`; const Preview: React.FC<PreviewProps> = () => { const layout = useAppSelector(selectLayout); diff --git a/ui/src/hooks/useComponentSelection.ts b/ui/src/hooks/useComponentSelection.ts index 2f0a20f2ca0c47d278e86684563bb428d203fa89..c2c35d1a8c6bff09338c52c6e3be2658ce2ceee9 100644 --- a/ui/src/hooks/useComponentSelection.ts +++ b/ui/src/hooks/useComponentSelection.ts @@ -17,8 +17,9 @@ import { import type { RegionNode } from '@/features/layout/layoutModelSlice'; import { selectLayout } from '@/features/layout/layoutModelSlice'; import { selectDevMode } from '@/features/configuration/configurationSlice'; +import { getXbSettings } from '@/utils/drupal-globals'; -const { drupalSettings } = window; +const xbSettings = getXbSettings(); /** * Filters out any components that are parents or children of components in the selection @@ -263,7 +264,7 @@ export function useComponentSelection() { }; // Add to Drupal settings for external access by extensions etc - drupalSettings.xb.componentSelectionUtils = componentSelectionUtils; + xbSettings.componentSelectionUtils = componentSelectionUtils; return componentSelectionUtils; } diff --git a/ui/src/hooks/useDrupalBehaviors.ts b/ui/src/hooks/useDrupalBehaviors.ts index 0b95f17b704a4bad50cd8e775786960f09deea20..a245db9f597143898f6be5f12553bda7323bc489 100644 --- a/ui/src/hooks/useDrupalBehaviors.ts +++ b/ui/src/hooks/useDrupalBehaviors.ts @@ -1,7 +1,8 @@ import { useEffect } from 'react'; import type { RefObject } from 'react'; +import { getDrupal } from '@/utils/drupal-globals'; -const { Drupal } = window as any; +const Drupal = getDrupal(); export function useDrupalBehaviors( ref: RefObject<HTMLElement>, diff --git a/ui/src/hooks/useEditorNavigation.ts b/ui/src/hooks/useEditorNavigation.ts index db468a93656c2c793e54a5b017161a836ce9fafa..e0ae3448af98c4bde1e173060ef4adf64cf57d72 100644 --- a/ui/src/hooks/useEditorNavigation.ts +++ b/ui/src/hooks/useEditorNavigation.ts @@ -1,9 +1,9 @@ import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { DEFAULT_REGION } from '@/features/ui/uiSlice'; -import { getBaseUrl } from '@/utils/drupal-globals'; +import { getBaseUrl, getXbSettings } from '@/utils/drupal-globals'; -const { drupalSettings } = window; +const xbSettings = getXbSettings(); /** * Hook for editor navigation functions @@ -43,7 +43,7 @@ export function useEditorNavigation() { setEditorEntity, }; - drupalSettings.xb.navUtils = editorNavUtils; + xbSettings.navUtils = editorNavUtils; return editorNavUtils; } diff --git a/ui/src/main.tsx b/ui/src/main.tsx index ce94c11ff98dcfe4255038d105cb5b17e1df6fa0..00d3fbc54edfd4148348e3899a4a15dc897cc1b0 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -20,6 +20,7 @@ import transforms from '@/utils/transforms'; import '@/styles/radix-themes'; import '@/styles/index.css'; import { AJAX_UPDATE_FORM_STATE_EVENT } from '@/types/Ajax'; +import { getBaseUrl, getDrupal, getXbSettings } from '@/utils/drupal-globals'; // Provide these dependencies as globals so extensions do not have redundant and // potentially conflicting dependencies. @@ -32,17 +33,18 @@ interface ProviderComponentProps { store: EnhancedStore; } -const { drupalSettings } = window; -const { Drupal } = window as any; +const Drupal = getDrupal(); +const xbSettings = getXbSettings(); +const baseUrl = getBaseUrl(); const container = document.getElementById('experience-builder'); const appConfiguration: AppConfiguration = { ...initialState, - baseUrl: drupalSettings?.path?.baseUrl || import.meta.env.BASE_URL, - entityType: drupalSettings?.xb?.entityType || 'node', - entity: drupalSettings?.xb?.entity || '1', - devMode: drupalSettings?.xb?.devMode || false, + baseUrl: baseUrl || import.meta.env.BASE_URL, + entityType: xbSettings.entityType || 'node', + entity: xbSettings.entity || '1', + devMode: xbSettings.devMode || false, }; const isAjaxing = () => @@ -88,13 +90,13 @@ Drupal.attachBehaviorsAfterAjaxing = attachBehaviorsAfterAjaxing; if (container) { const root = createRoot(container); let routerRoot = appConfiguration.baseUrl; - if (drupalSettings?.xb?.base) { - routerRoot = `${routerRoot}${drupalSettings.xb.base}`; + if (xbSettings.base) { + routerRoot = `${routerRoot}${xbSettings.base}`; } const store = makeStore({ configuration: appConfiguration }); // Make the store available to extensions. - (drupalSettings as any).xb.store = store; + xbSettings.store = store; root.render( <React.StrictMode> diff --git a/ui/src/services/addAjaxPageState.ts b/ui/src/services/addAjaxPageState.ts index 4f0af374877d79baedc58695c24f960e84b34ae2..fd082b8a3c7ba65213f7a157cbc15aaf9f02b107 100644 --- a/ui/src/services/addAjaxPageState.ts +++ b/ui/src/services/addAjaxPageState.ts @@ -1,4 +1,6 @@ -const { drupalSettings } = window as any; +import { getDrupalSettings } from '@/utils/drupal-globals'; + +const drupalSettings = getDrupalSettings(); const addAjaxPageState = (query: string) => { // Drupal's AJAX API automatically adds ajaxPageState as a parameter, but diff --git a/ui/src/services/processResponseAssets.ts b/ui/src/services/processResponseAssets.ts index 3c9f54e8c5774a28b6edcdba5260b3c7863b16b4..c8c88430aa4488fa16832f203dcbdfdeee9c9e5c 100644 --- a/ui/src/services/processResponseAssets.ts +++ b/ui/src/services/processResponseAssets.ts @@ -1,6 +1,7 @@ import type { PropsValues } from '@/types/Form'; +import { getDrupal } from '@/utils/drupal-globals'; -const { Drupal } = window as any; +const Drupal = getDrupal(); /** * Takes a response rendered by XBTemplateRenderer, identifies any attached diff --git a/ui/src/types/DrupalSettings.ts b/ui/src/types/DrupalSettings.ts new file mode 100644 index 0000000000000000000000000000000000000000..f817332eccc1c40339c20148a342a9c8c2cd8900 --- /dev/null +++ b/ui/src/types/DrupalSettings.ts @@ -0,0 +1,39 @@ +import type { PropsValues } from '@/types/Form'; +import type { FormatType } from '@/types/FormatType'; + +export interface DrupalSettings { + xb: { + base: string; + entityType: string; + entity: string; + entityTypeKeys: { + id: string; + label: string; + }; + globalAssets: { + css: string; + jsHeader: string; + jsFooter: string; + }; + layoutUtils: PropsValues; + componentSelectionUtils: PropsValues; + navUtils: PropsValues; + xbModulePath: string; + selectedComponent: string; + devMode: boolean; + }; + xbExtension: object; + path: { + baseUrl: string; + }; + editor: { + formats: { + [key: string]: FormatType; + }; + }; + ajaxPageState: { + libraries: string; + theme: string; + theme_token: string; + }; +} diff --git a/ui/src/types/FormatType.ts b/ui/src/types/FormatType.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab62c4c5340021fac0bbd8639f33b5c0e4fe57ed --- /dev/null +++ b/ui/src/types/FormatType.ts @@ -0,0 +1,13 @@ +export interface FormatType { + format: string; + editor?: string; + editorSettings?: { + toolbar: any[]; + plugins: string[]; + config: { + [key: string]: any; + }; + language: Record<string, any>; + }; + [key: string]: any; +} diff --git a/ui/src/utils/drupal-globals.ts b/ui/src/utils/drupal-globals.ts index 574755467f6ef502d97f7b684895c709beb55cdf..fecd4257e9f6f6460770b021491641526cf2bf47 100644 --- a/ui/src/utils/drupal-globals.ts +++ b/ui/src/utils/drupal-globals.ts @@ -1,7 +1,18 @@ -// @todo Refactor codebase to use these methods in https://drupal.org/i/3521811. +import type { PropsValues } from '@/types/Form'; +import type { DrupalSettings } from '@/types/DrupalSettings'; + const { Drupal, drupalSettings } = window as any; export const getDrupal = () => Drupal; -export const getDrupalSettings = () => drupalSettings; +export const getDrupalSettings = (): DrupalSettings => drupalSettings; export const getXbSettings = () => drupalSettings.xb; export const getBaseUrl = () => drupalSettings.path.baseUrl; + +export const setXbDrupalSetting = ( + property: 'layoutUtils' | 'navUtils', + value: PropsValues, +) => { + if (drupalSettings?.xb?.[property]) { + drupalSettings.xb[property] = { ...drupalSettings.xb[property], ...value }; + } +}; diff --git a/ui/tests/vitest/support/vitest.setup.js b/ui/tests/vitest/support/vitest.setup.js index 08aae24d56c1b5b16fe9ad59f8bb8763eb244c4a..d9adef1345346cca05e72dd39d02b665c74fd306 100644 --- a/ui/tests/vitest/support/vitest.setup.js +++ b/ui/tests/vitest/support/vitest.setup.js @@ -1,5 +1,12 @@ import { vi } from 'vitest'; +const mockDrupalSettings = { + path: { + baseUrl: '/', + }, + xb: {}, +}; + vi.stubGlobal('URL', { createObjectURL: vi.fn().mockImplementation((blob) => { return `mock-object-url/${blob.name}`; @@ -10,14 +17,17 @@ vi.mock('@/utils/drupal-globals', () => ({ getDrupal: () => ({ url: (path) => `http://mock-drupal-url/${path}`, }), - getDrupalSettings: () => ({ - path: { - baseUrl: '/', - }, - xb: {}, - }), - getXbSettings: () => ({}), - getBasePath: () => '/', + getDrupalSettings: () => mockDrupalSettings, + getXbSettings: () => mockDrupalSettings.xb, + getBasePath: () => mockDrupalSettings.path.baseUrl, + setXbDrupalSetting: (property, value) => { + if (mockDrupalSettings?.xb?.[property]) { + mockDrupalSettings.xb[property] = { + ...mockDrupalSettings.xb[property], + ...value, + }; + } + }, })); vi.mock('@swc/wasm-web', () => ({