diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-builder.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-builder.component.ts index c32121bcfc53..953c1a60c8b7 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-builder.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-builder.component.ts @@ -26,14 +26,9 @@ import { DotMessageService } from '@dotcms/data-access'; import { DotCMSContentType, DotMessageSeverity, DotMessageType } from '@dotcms/dotcms-models'; +import { StyleEditorFieldSchema, StyleEditorFormSchema } from '@dotcms/types/internal'; import { DotMessagePipe } from '@dotcms/ui'; -import { - StyleEditorField, - StyleEditorFieldSchema, - StyleEditorFormSchema, - defineStyleEditorSchema, - styleEditorField -} from '@dotcms/uve'; +import { StyleEditorField, defineStyleEditorSchema, styleEditorField } from '@dotcms/uve/internal'; import { DotStyleEditorSectionComponent } from './dot-style-editor-section.component'; import { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-field-form.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-field-form.component.ts index 70ead34043a7..78c4da5f6ca6 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-field-form.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/dot-style-editor-field-form.component.ts @@ -20,8 +20,8 @@ import { SelectModule } from 'primeng/select'; import { SelectButtonModule } from 'primeng/selectbutton'; import { DotMessageService } from '@dotcms/data-access'; +import { StyleEditorFieldType } from '@dotcms/types/internal'; import { DotMessagePipe } from '@dotcms/ui'; -import { StyleEditorFieldType } from '@dotcms/uve'; import { BuilderField, BuilderOption, FIELD_TYPE_OPTIONS, toLabelIdentifier } from './models'; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/models.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/models.ts index a8fa5047a086..5d6eb191268a 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/models.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/style-editor/models.ts @@ -1,4 +1,4 @@ -import { StyleEditorFieldType } from '@dotcms/uve'; +import { StyleEditorFieldType } from '@dotcms/types/internal'; export interface BuilderOption { label: string; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-checkbox-group/uve-style-editor-field-checkbox-group.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-checkbox-group/uve-style-editor-field-checkbox-group.component.ts index f30d1f305c1b..36754a40aae9 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-checkbox-group/uve-style-editor-field-checkbox-group.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-checkbox-group/uve-style-editor-field-checkbox-group.component.ts @@ -3,7 +3,7 @@ import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { Checkbox } from 'primeng/checkbox'; -import { StyleEditorFieldSchema, StyleEditorRadioOptionObject } from '@dotcms/uve'; +import { StyleEditorFieldSchema, StyleEditorRadioOptionObject } from '@dotcms/types/internal'; @Component({ selector: 'dot-uve-style-editor-field-checkbox-group', diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-dropdown/uve-style-editor-field-dropdown.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-dropdown/uve-style-editor-field-dropdown.component.ts index 74f7571a5d35..26a26c665043 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-dropdown/uve-style-editor-field-dropdown.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-dropdown/uve-style-editor-field-dropdown.component.ts @@ -1,9 +1,9 @@ -import { Component, input, inject, computed } from '@angular/core'; +import { Component, computed, inject, input } from '@angular/core'; import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { Select } from 'primeng/select'; -import { StyleEditorFieldSchema, StyleEditorRadioOptionObject } from '@dotcms/uve'; +import { StyleEditorFieldSchema, StyleEditorRadioOptionObject } from '@dotcms/types/internal'; @Component({ selector: 'dot-uve-style-editor-field-dropdown', diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-input/uve-style-editor-field-input.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-input/uve-style-editor-field-input.component.ts index 71fb6312d11d..bcd4711f9426 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-input/uve-style-editor-field-input.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-input/uve-style-editor-field-input.component.ts @@ -1,10 +1,10 @@ -import { Component, input, inject } from '@angular/core'; +import { Component, inject, input } from '@angular/core'; import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { InputTextModule } from 'primeng/inputtext'; +import { StyleEditorFieldSchema } from '@dotcms/types/internal'; import { DotMessagePipe } from '@dotcms/ui'; -import { StyleEditorFieldSchema } from '@dotcms/uve'; @Component({ selector: 'dot-uve-style-editor-field-input', diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-radio/uve-style-editor-field-radio.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-radio/uve-style-editor-field-radio.component.ts index 75c0a5ea683c..daf9c1e6f84c 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-radio/uve-style-editor-field-radio.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/components/uve-style-editor-field-radio/uve-style-editor-field-radio.component.ts @@ -1,9 +1,9 @@ -import { Component, input, inject, computed } from '@angular/core'; +import { Component, computed, inject, input } from '@angular/core'; import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { RadioButtonModule } from 'primeng/radiobutton'; -import { StyleEditorFieldSchema, StyleEditorRadioOptionObject } from '@dotcms/uve'; +import { StyleEditorFieldSchema, StyleEditorRadioOptionObject } from '@dotcms/types/internal'; @Component({ selector: 'dot-uve-style-editor-field-radio', diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/dot-uve-style-editor-form.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/dot-uve-style-editor-form.component.ts index 20682136d8f9..e76f50ee97b1 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/dot-uve-style-editor-form.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/dot-uve-style-editor-form.component.ts @@ -23,7 +23,7 @@ import { debounce, distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs import { DotMessageService } from '@dotcms/data-access'; import { StyleEditorProperties } from '@dotcms/types'; -import { StyleEditorFormSchema } from '@dotcms/uve'; +import { StyleEditorFormSchema } from '@dotcms/types/internal'; import { UveStyleEditorFieldCheckboxGroupComponent } from './components/uve-style-editor-field-checkbox-group/uve-style-editor-field-checkbox-group.component'; import { UveStyleEditorFieldDropdownComponent } from './components/uve-style-editor-field-dropdown/uve-style-editor-field-dropdown.component'; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/services/style-editor-form-builder.service.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/services/style-editor-form-builder.service.ts index 19ee57b7f85c..a4abed681725 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/services/style-editor-form-builder.service.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-palette/components/dot-uve-style-editor-form/services/style-editor-form-builder.service.ts @@ -3,11 +3,11 @@ import { AbstractControl, FormBuilder, FormControl, FormGroup } from '@angular/f import { StyleEditorProperties } from '@dotcms/types'; import { - StyleEditorCheckboxDefaultValue, StyleEditorFieldSchema, StyleEditorFormSchema, StyleEditorSectionSchema -} from '@dotcms/uve'; +} from '@dotcms/types/internal'; +import { StyleEditorCheckboxDefaultValue } from '@dotcms/uve/internal'; import { STYLE_EDITOR_FIELD_TYPES } from '../../../../../../shared/consts'; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts index 930516ca29d1..3641a8eeabf3 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts @@ -56,10 +56,9 @@ import { } from '@dotcms/dotcms-models'; import { DotResultsSeoToolComponent } from '@dotcms/portlets/dot-ema/ui'; import { DotCMSPage, DotCMSURLContentMap, DotCMSUVEAction, UVE_MODE } from '@dotcms/types'; -import { __DOTCMS_UVE_EVENT__ } from '@dotcms/types/internal'; +import { StyleEditorFormSchema, __DOTCMS_UVE_EVENT__ } from '@dotcms/types/internal'; import { DotCopyContentModalService, DotMessagePipe } from '@dotcms/ui'; import { WINDOW, isEqual } from '@dotcms/utils'; -import { StyleEditorFormSchema } from '@dotcms/uve'; import { getContentletsInContainer } from '@dotcms/uve/internal'; import { DotUveContentletQuickEditComponent } from './components/dot-uve-contentlet-quick-edit/dot-uve-contentlet-quick-edit.component'; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-actions-handler/dot-uve-actions-handler.service.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-actions-handler/dot-uve-actions-handler.service.ts index 5ac1c4a95431..46761f3c5c21 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-actions-handler/dot-uve-actions-handler.service.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-actions-handler/dot-uve-actions-handler.service.ts @@ -1,7 +1,7 @@ import { tapResponse } from '@ngrx/operators'; import { Observable, of } from 'rxjs'; -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { MessageService } from 'primeng/api'; @@ -14,9 +14,8 @@ import { DotCMSInlineEditingType, DotCMSUVEAction } from '@dotcms/types'; -import { __DOTCMS_UVE_EVENT__ } from '@dotcms/types/internal'; +import { __DOTCMS_UVE_EVENT__, StyleEditorFormSchema } from '@dotcms/types/internal'; import { DotCopyContentModalService } from '@dotcms/ui'; -import { StyleEditorFormSchema } from '@dotcms/uve'; import { DotBlockEditorSidebarComponent } from '../../components/dot-block-editor-sidebar/dot-block-editor-sidebar.component'; import { DotEmaDialogComponent } from '../../components/dot-ema-dialog/dot-ema-dialog.component'; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts index 2fe5729f4e24..a239c9485480 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts @@ -1,6 +1,6 @@ import { DotDeviceListItem, FeaturedFlags } from '@dotcms/dotcms-models'; import { DotCMSViewAsPersona } from '@dotcms/types'; -import { StyleEditorFieldType } from '@dotcms/uve'; +import { StyleEditorFieldType } from '@dotcms/types/internal'; import { CommonErrors } from './enums'; import { CommonErrorsInfo } from './models'; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts index 04e5129ff7fc..b9d1a1b6d4d4 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts @@ -1,6 +1,6 @@ import { DotDeviceListItem, SeoMetaTags, SeoMetaTagsResult } from '@dotcms/dotcms-models'; import { DotCMSViewAsPersona } from '@dotcms/types'; -import { StyleEditorFormSchema } from '@dotcms/uve'; +import { StyleEditorFormSchema } from '@dotcms/types/internal'; import { Container, diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts index 98e6a64205bc..648e647cbfae 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts @@ -9,8 +9,8 @@ import { SeoMetaTagsResult } from '@dotcms/dotcms-models'; import { UVE_MODE } from '@dotcms/types'; +import { StyleEditorFormSchema } from '@dotcms/types/internal'; import { WINDOW } from '@dotcms/utils'; -import { StyleEditorFormSchema } from '@dotcms/uve'; import { PageData, PageDataContainer, ReloadEditorContent } from './models'; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts index 4212792dc680..57a6331f8c8b 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts @@ -7,27 +7,33 @@ import { take } from 'rxjs/operators'; import { DotPropertiesService } from '@dotcms/data-access'; import { FeaturedFlags } from '@dotcms/dotcms-models'; -import { WithFlagsState } from './models'; +import { UVEFlags, WithFlagsState } from './models'; import { UVEState } from '../../models'; /** * - * @description This feature is used to handle the fetch of flags + * @description Loads feature flags from configuration, then forces `FEATURE_FLAG_UVE_STYLE_EDITOR` to `true`. * @export * @return {*} */ -export function withFlags(flags: FeaturedFlags[]) { +export function withFlags(featureFlagKeys: FeaturedFlags[]) { return signalStoreFeature( { state: type() }, withState({ flags: {} }), withHooks({ onInit: (store) => { const propertiesService = inject(DotPropertiesService); + propertiesService - .getFeatureFlags(flags) + .getFeatureFlags(featureFlagKeys) .pipe(take(1)) - .subscribe((flags) => { + .subscribe((fetchedFlags) => { + // TODO: Remove this, only harcoded until the PR that fix is merged + const flags: UVEFlags = { ...(fetchedFlags as unknown as UVEFlags) }; + + flags[FeaturedFlags.FEATURE_FLAG_UVE_STYLE_EDITOR] = true; + patchState(store, { flags }); }); } diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts index 55bf97a0f520..1281697ce7e6 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts @@ -8,7 +8,7 @@ import { SeoMetaTagsResult } from '@dotcms/dotcms-models'; import { DotCMSPage } from '@dotcms/types'; -import { StyleEditorFormSchema } from '@dotcms/uve'; +import { StyleEditorFormSchema } from '@dotcms/types/internal'; import { UVEFlags } from './features/flags/models'; diff --git a/core-web/libs/sdk/angular/src/lib/services/dotcms-editable-page.service.ts b/core-web/libs/sdk/angular/src/lib/services/dotcms-editable-page.service.ts index 326f6b071be6..2a057f65ba31 100644 --- a/core-web/libs/sdk/angular/src/lib/services/dotcms-editable-page.service.ts +++ b/core-web/libs/sdk/angular/src/lib/services/dotcms-editable-page.service.ts @@ -11,6 +11,7 @@ import { DotCMSExtendedPageResponse } from '@dotcms/types'; import { createUVESubscription, getUVEState, initUVE, updateNavigation } from '@dotcms/uve'; +import { registerStyleEditorSchemas } from '@dotcms/uve/internal'; @Injectable({ providedIn: 'root' @@ -82,6 +83,10 @@ export class DotCMSEditablePageService { updateNavigation(pageURI); } + if (response?.styleEditorSchemas?.length) { + registerStyleEditorSchemas(response.styleEditorSchemas); + } + const unsubscribeUVEChanges = this.#listenUVEChanges(); return this.#response$.pipe( diff --git a/core-web/libs/sdk/client/src/lib/client/page/page-api.ts b/core-web/libs/sdk/client/src/lib/client/page/page-api.ts index ef931ba3d501..884b9c4f2eb6 100644 --- a/core-web/libs/sdk/client/src/lib/client/page/page-api.ts +++ b/core-web/libs/sdk/client/src/lib/client/page/page-api.ts @@ -2,21 +2,338 @@ import { consola } from 'consola'; import { DotCMSClientConfig, + DotCMSComposedPageResponse, + DotCMSExtendedPageResponse, DotCMSPageRequestParams, DotCMSPageResponse, - DotCMSExtendedPageResponse, - DotCMSComposedPageResponse, + DotErrorPage, DotHttpClient, - DotRequestOptions, DotHttpError, - DotErrorPage + DotRequestOptions } from '@dotcms/types'; +import { StyleEditorFormSchema } from '@dotcms/types/internal'; import { buildPageQuery, buildQuery, fetchGraphQL, mapContentResponse } from './utils'; import { graphqlToPageEntity } from '../../utils'; import { BaseApiClient } from '../base/api/base-api'; +/** + * Fetches style editor schemas for the given page URL. + * + * TODO: Replace mock with real endpoint call like: + * GET /api/v1/style-editor/schemas?pageUrl= + * + * @internal + */ +async function fetchStyleEditorSchemas( + _url: string, + _config: DotCMSClientConfig, + _requestOptions: DotRequestOptions, + _httpClient: DotHttpClient +): Promise { + // TODO: Replace mock with real endpoint call like: + // GET /api/v1/style-editor/schemas?pageUrl= + return Promise.resolve([ + { + contentType: 'Activity', + sections: [ + { + title: 'Typography', + fields: [ + { + type: 'dropdown', + label: 'Title Size', + id: 'title-size', + config: { + options: [ + { + label: 'Small', + value: 'text-lg' + }, + { + label: 'Medium', + value: 'text-xl' + }, + { + label: 'Large', + value: 'text-2xl' + }, + { + label: 'Extra Large', + value: 'text-3xl' + } + ] + } + }, + { + type: 'dropdown', + label: 'Description Size', + id: 'description-size', + config: { + options: [ + { + label: 'Small', + value: 'text-sm' + }, + { + label: 'Medium', + value: 'text-base' + }, + { + label: 'Large', + value: 'text-lg' + } + ] + } + }, + { + type: 'checkboxGroup', + label: 'Title Style', + id: 'title-style', + config: { + options: [ + { + label: 'Bold', + value: 'bold' + }, + { + label: 'Italic', + value: 'italic' + }, + { + label: 'Underline', + value: 'underline' + } + ] + } + } + ] + }, + { + title: 'Layout', + fields: [ + { + type: 'radio', + label: 'Layout', + id: 'layout', + config: { + options: [ + { + label: 'Left', + value: 'left', + imageURL: + 'https://i.ibb.co/cXv3tfYd/Screenshot-2025-12-23-at-11-58-32-AM.png' + }, + { + label: 'Right', + value: 'right', + imageURL: + 'https://i.ibb.co/v4cJxyLZ/Screenshot-2025-12-23-at-11-59-01-AM.png' + }, + { + label: 'Center', + value: 'center', + imageURL: + 'https://i.ibb.co/kVntSyzn/Screenshot-2025-12-23-at-11-58-50-AM.png' + }, + { + label: 'Overlap', + value: 'overlap', + imageURL: + 'https://i.ibb.co/43Y5KLY/placeholder-icon-design-free-vector.jpg' + } + ], + columns: 2 + } + }, + { + type: 'dropdown', + label: 'Image Height', + id: 'image-height', + config: { + options: [ + { + label: 'Small', + value: 'h-40' + }, + { + label: 'Medium', + value: 'h-56' + }, + { + label: 'Large', + value: 'h-72' + }, + { + label: 'Extra Large', + value: 'h-96' + } + ] + } + } + ] + }, + { + title: 'Card Style', + fields: [ + { + type: 'radio', + label: 'Card Background', + id: 'card-background', + config: { + options: [ + { + label: 'White', + value: 'white' + }, + { + label: 'Gray', + value: 'gray' + }, + { + label: 'Light Blue', + value: 'light-blue' + }, + { + label: 'Light Green', + value: 'light-green' + } + ], + columns: 2 + } + }, + { + type: 'radio', + label: 'Border Radius', + id: 'border-radius', + config: { + options: [ + { + label: 'None', + value: 'none' + }, + { + label: 'Small', + value: 'small' + }, + { + label: 'Medium', + value: 'medium' + }, + { + label: 'Large', + value: 'large' + } + ], + columns: 2 + } + }, + { + type: 'checkboxGroup', + label: 'Card Effects', + id: 'card-effects', + config: { + options: [ + { + label: 'Shadow', + value: 'shadow' + }, + { + label: 'Border', + value: 'border' + } + ] + } + } + ] + }, + { + title: 'Button', + fields: [ + { + type: 'radio', + label: 'Button Color', + id: 'button-color', + config: { + options: [ + { + label: 'Blue', + value: 'blue' + }, + { + label: 'Green', + value: 'green' + }, + { + label: 'Red', + value: 'red' + }, + { + label: 'Purple', + value: 'purple' + }, + { + label: 'Orange', + value: 'orange' + }, + { + label: 'Teal', + value: 'teal' + } + ], + columns: 2 + } + }, + { + type: 'dropdown', + label: 'Button Size', + id: 'button-size', + config: { + options: [ + { + label: 'Small', + value: 'small' + }, + { + label: 'Medium', + value: 'medium' + }, + { + label: 'Large', + value: 'large' + } + ] + } + }, + { + type: 'checkboxGroup', + label: 'Button Style', + id: 'button-style', + config: { + options: [ + { + label: 'Rounded', + value: 'rounded' + }, + { + label: 'Full Rounded', + value: 'full-rounded' + }, + { + label: 'Shadow', + value: 'shadow' + } + ] + } + } + ] + } + ] + } + ]); +} + /** * Client for interacting with the DotCMS Page API. * Provides methods to retrieve and manipulate pages. @@ -144,12 +461,18 @@ export class PageClient extends BaseApiClient { const requestBody = JSON.stringify({ query: completeQuery, variables: requestVariables }); try { - const response = await fetchGraphQL({ - baseURL: this.dotcmsUrl, - body: requestBody, - headers: requestHeaders, - httpClient: this.httpClient - }); + const [response, styleEditorSchemas] = await Promise.all([ + fetchGraphQL({ + baseURL: this.dotcmsUrl, + body: requestBody, + headers: requestHeaders, + httpClient: this.httpClient + }), + fetchStyleEditorSchemas(url, this.config, this.requestOptions, this.httpClient) + ]); + + console.log('styleEditorSchemas', styleEditorSchemas); + // The GQL endpoint can return errors and data, we need to handle both if (response.errors) { response.errors.forEach((error: { message: string }) => { @@ -191,7 +514,8 @@ export class PageClient extends BaseApiClient { graphql: { query: completeQuery, variables: requestVariables - } + }, + ...(styleEditorSchemas && { styleEditorSchemas }) }; } catch (error) { // Handle DotHttpError instances diff --git a/core-web/libs/sdk/react/src/index.server.ts b/core-web/libs/sdk/react/src/index.server.ts index 649e57cdebe4..f257baeee02e 100644 --- a/core-web/libs/sdk/react/src/index.server.ts +++ b/core-web/libs/sdk/react/src/index.server.ts @@ -1,6 +1,6 @@ // Server-safe entry point — excludes client-only modules that import // browser APIs or class components (e.g. TinyMCE) which break SSR. -// Hooks (useEditableDotCMSPage, useDotCMSShowWhen, useAISearch, useStyleEditorSchemas) +// Hooks (useEditableDotCMSPage, useDotCMSShowWhen, useAISearch) // are intentionally excluded: they use useState/useEffect and have no 'use client' // directive, so including them here would pull React client APIs into the RSC module // graph and cause a build error in Next.js App Router. diff --git a/core-web/libs/sdk/react/src/index.ts b/core-web/libs/sdk/react/src/index.ts index 6d5116f6c64a..9bd80e1b4dbb 100644 --- a/core-web/libs/sdk/react/src/index.ts +++ b/core-web/libs/sdk/react/src/index.ts @@ -22,8 +22,6 @@ export type { DotCMSLayoutBodyProps } from './lib/next/components/DotCMSLayoutBo export { useAISearch } from './lib/next/hooks/useAISearch'; -export { useStyleEditorSchemas } from './lib/next/hooks/useStyleEditorSchemas'; - //Export AI types from shared types export type { DotCMSAISearchValue, DotCMSAISearchProps } from './lib/next/shared/types'; diff --git a/core-web/libs/sdk/react/src/lib/next/__test__/hook/useStyleEditorSchemas.test.tsx b/core-web/libs/sdk/react/src/lib/next/__test__/hook/useStyleEditorSchemas.test.tsx deleted file mode 100644 index 76db37519235..000000000000 --- a/core-web/libs/sdk/react/src/lib/next/__test__/hook/useStyleEditorSchemas.test.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; - -import { StyleEditorFormSchema, registerStyleEditorSchemas } from '@dotcms/uve'; - -import { useStyleEditorSchemas } from '../../hooks/useStyleEditorSchemas'; - -jest.mock('@dotcms/uve', () => ({ - registerStyleEditorSchemas: jest.fn(), - StyleEditorFormSchema: {} -})); - -describe('useStyleEditorSchemas', () => { - const registerStyleEditorSchemasMock = registerStyleEditorSchemas as jest.Mock; - - const mockForm1: StyleEditorFormSchema = { - contentType: 'BlogPost', - sections: [ - { - title: 'Typography', - fields: [ - { - type: 'input', - label: 'Font Size', - config: { - inputType: 'number', - defaultValue: 1 - } - } - ] - } - ] - }; - - const mockForm2: StyleEditorFormSchema = { - contentType: 'Banner', - sections: [ - { - title: 'Colors', - fields: [ - { - type: 'input', - label: 'Background Color', - config: { - inputType: 'text', - defaultValue: '#FFFFFF' - } - } - ] - } - ] - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('should call registerStyleEditorSchemas with provided forms on mount', () => { - const forms = [mockForm1]; - - renderHook(() => useStyleEditorSchemas(forms)); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - expect(registerStyleEditorSchemasMock).toHaveBeenCalledWith(forms); - }); - - test('should call registerStyleEditorSchemas with multiple forms', () => { - const forms = [mockForm1, mockForm2]; - - renderHook(() => useStyleEditorSchemas(forms)); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - expect(registerStyleEditorSchemasMock).toHaveBeenCalledWith(forms); - }); - - test('should call registerStyleEditorSchemas with empty array', () => { - const forms: StyleEditorFormSchema[] = []; - - renderHook(() => useStyleEditorSchemas(forms)); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - expect(registerStyleEditorSchemasMock).toHaveBeenCalledWith(forms); - }); - - test('should re-register forms when forms array changes', () => { - const initialForms = [mockForm1]; - const { rerender } = renderHook(({ forms }) => useStyleEditorSchemas(forms), { - initialProps: { forms: initialForms } - }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - expect(registerStyleEditorSchemasMock).toHaveBeenCalledWith(initialForms); - - const updatedForms = [mockForm1, mockForm2]; - rerender({ forms: updatedForms }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(2); - expect(registerStyleEditorSchemasMock).toHaveBeenLastCalledWith(updatedForms); - }); - - test('should re-register forms when forms array reference changes even with same content', () => { - const forms1 = [mockForm1]; - const forms2 = [mockForm1]; // Same content, different reference - - const { rerender } = renderHook(({ forms }) => useStyleEditorSchemas(forms), { - initialProps: { forms: forms1 } - }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - - rerender({ forms: forms2 }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(2); - expect(registerStyleEditorSchemasMock).toHaveBeenNthCalledWith(1, forms1); - expect(registerStyleEditorSchemasMock).toHaveBeenNthCalledWith(2, forms2); - }); - - test('should not re-register when forms array reference does not change', () => { - const forms = [mockForm1]; - - const { rerender } = renderHook(({ forms }) => useStyleEditorSchemas(forms), { - initialProps: { forms } - }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - - // Rerender with same forms reference - rerender({ forms }); - - // Should not call again because forms reference hasn't changed - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - }); - - test('should handle forms array being replaced with empty array', () => { - const initialForms = [mockForm1, mockForm2]; - const { rerender } = renderHook(({ forms }) => useStyleEditorSchemas(forms), { - initialProps: { forms: initialForms } - }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - expect(registerStyleEditorSchemasMock).toHaveBeenCalledWith(initialForms); - - const emptyForms: StyleEditorFormSchema[] = []; - rerender({ forms: emptyForms }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(2); - expect(registerStyleEditorSchemasMock).toHaveBeenLastCalledWith(emptyForms); - }); - - test('should handle forms array being replaced with different forms', () => { - const initialForms = [mockForm1]; - const { rerender } = renderHook(({ forms }) => useStyleEditorSchemas(forms), { - initialProps: { forms: initialForms } - }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - - const differentForms = [mockForm2]; - rerender({ forms: differentForms }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(2); - expect(registerStyleEditorSchemasMock).toHaveBeenLastCalledWith(differentForms); - }); - - test('should cleanup and not call registerStyleEditorSchemas after unmount', () => { - const forms = [mockForm1]; - const { unmount } = renderHook(() => useStyleEditorSchemas(forms)); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - - unmount(); - - // Should not call again after unmount - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - }); - - describe('infinite loop prevention', () => { - test('should not cause infinite loop when inline object is passed on each render', () => { - // Simulate a component that passes inline forms on each render - // This creates a new reference each time, which could cause infinite loops - const { rerender } = renderHook( - () => - useStyleEditorSchemas([ - { - contentType: 'InlineForm', - sections: [ - { - title: 'Inline Section', - fields: [ - { - type: 'input', - label: 'Inline Field', - config: { inputType: 'text' } - } - ] - } - ] - } - ]), - {} - ); - - // First render - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - - // Simulate multiple re-renders (as would happen in a real component) - rerender(); - rerender(); - rerender(); - - // With current implementation, each rerender with inline object creates new reference - // This is expected behavior - the hook re-registers on each new reference - // The key is that it doesn't cause an INFINITE loop (exponential calls) - // It should be called once per render, not exponentially - const callCount = registerStyleEditorSchemasMock.mock.calls.length; - - // Should be exactly 4 calls (1 initial + 3 rerenders), not exponentially growing - expect(callCount).toBe(4); - - // Additional rerenders should add linearly, not exponentially - rerender(); - rerender(); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(6); - }); - - test('should demonstrate stable reference prevents unnecessary re-registrations', () => { - // This test shows the recommended pattern: memoize forms to prevent re-registration - const stableForms = [mockForm1]; - - const { rerender } = renderHook(({ forms }) => useStyleEditorSchemas(forms), { - initialProps: { forms: stableForms } - }); - - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - - // Multiple rerenders with same reference should NOT re-register - rerender({ forms: stableForms }); - rerender({ forms: stableForms }); - rerender({ forms: stableForms }); - - // Should still be 1 - stable reference prevents re-registration - expect(registerStyleEditorSchemasMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/core-web/libs/sdk/react/src/lib/next/hooks/useEditableDotCMSPage.ts b/core-web/libs/sdk/react/src/lib/next/hooks/useEditableDotCMSPage.ts index abe8b8f9a59f..af9aa3d69c2b 100644 --- a/core-web/libs/sdk/react/src/lib/next/hooks/useEditableDotCMSPage.ts +++ b/core-web/libs/sdk/react/src/lib/next/hooks/useEditableDotCMSPage.ts @@ -6,6 +6,7 @@ import { DotCMSExtendedPageResponse } from '@dotcms/types'; import { getUVEState, initUVE, createUVESubscription, updateNavigation } from '@dotcms/uve'; +import { registerStyleEditorSchemas } from '@dotcms/uve/internal'; /** * Custom hook to manage the editable state of a DotCMS page. @@ -123,6 +124,10 @@ export const useEditableDotCMSPage = ( updateNavigation(pageURI); } + if (pageResponse.styleEditorSchemas?.length) { + registerStyleEditorSchemas(pageResponse.styleEditorSchemas); + } + return () => { destroyUVESubscriptions(); }; diff --git a/core-web/libs/sdk/react/src/lib/next/hooks/useStyleEditorSchemas.ts b/core-web/libs/sdk/react/src/lib/next/hooks/useStyleEditorSchemas.ts deleted file mode 100644 index e3ffa06379d1..000000000000 --- a/core-web/libs/sdk/react/src/lib/next/hooks/useStyleEditorSchemas.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useEffect } from 'react'; - -import { registerStyleEditorSchemas, StyleEditorFormSchema } from '@dotcms/uve'; - -/** - * Hook to register style editor forms with the UVE editor. - * @param forms - Array of style editor form schemas to register - * @returns void - */ -export const useStyleEditorSchemas = (styleEditorForms: StyleEditorFormSchema[]): void => { - useEffect(() => { - registerStyleEditorSchemas(styleEditorForms); - }, [styleEditorForms]); -}; diff --git a/core-web/libs/sdk/types/src/internal.ts b/core-web/libs/sdk/types/src/internal.ts index 9bdeb37629f8..cca60d7c9d3e 100644 --- a/core-web/libs/sdk/types/src/internal.ts +++ b/core-web/libs/sdk/types/src/internal.ts @@ -10,3 +10,6 @@ export * from './lib/ai/internal'; // Client export * from './lib/client/internal'; + +// Style Editor +export * from './lib/style-editor/internal'; diff --git a/core-web/libs/sdk/types/src/lib/page/public.ts b/core-web/libs/sdk/types/src/lib/page/public.ts index 97dc5dcccb70..3634e410f9be 100644 --- a/core-web/libs/sdk/types/src/lib/page/public.ts +++ b/core-web/libs/sdk/types/src/lib/page/public.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { DotHttpError } from '../client/public'; +import { StyleEditorFormSchema } from '../style-editor/internal'; /** * Represents a map of style property keys and their corresponding values @@ -1200,6 +1201,7 @@ export interface DotCMSPageResponse { query: string; variables: Record; }; + styleEditorSchemas?: StyleEditorFormSchema[]; } // Pick only the page and content properties to be able to extend these properties, they are optional diff --git a/core-web/libs/sdk/types/src/lib/style-editor/internal.ts b/core-web/libs/sdk/types/src/lib/style-editor/internal.ts new file mode 100644 index 000000000000..28c0c3ef19b3 --- /dev/null +++ b/core-web/libs/sdk/types/src/lib/style-editor/internal.ts @@ -0,0 +1,79 @@ +/** + * Available field types for the style editor. + */ +export type StyleEditorFieldType = 'input' | 'dropdown' | 'radio' | 'checkboxGroup'; + +/** + * Available input types for input fields in the style editor. + */ +export type StyleEditorFieldInputType = 'text' | 'number'; + +/** + * Base option object with label and value properties. + */ +export interface StyleEditorOptionObject { + label: string; + value: string; +} + +/** + * Extended option object for radio fields with optional image support. + */ +export interface StyleEditorRadioOptionObject extends StyleEditorOptionObject { + imageURL?: string; +} + +/** + * Option type for dropdown fields. + */ +export type StyleEditorOption = StyleEditorOptionObject; + +/** + * Option type for radio fields (supports optional image). + */ +export type StyleEditorRadioOption = StyleEditorRadioOptionObject; + +/** + * Checkbox option with a key identifier instead of value. + */ +export interface StyleEditorCheckboxOption { + label: string; + key: string; +} + +/** + * Configuration object for normalized field schemas sent to UVE. + */ +export interface StyleEditorFieldSchemaConfig { + inputType?: StyleEditorFieldInputType; + placeholder?: string; + options?: StyleEditorRadioOptionObject[]; + columns?: 1 | 2; +} + +/** + * Normalized field schema sent to UVE. + */ +export interface StyleEditorFieldSchema { + id: string; + type: StyleEditorFieldType; + label: string; + config: StyleEditorFieldSchemaConfig; +} + +/** + * Normalized section schema sent to UVE. + */ +export interface StyleEditorSectionSchema { + title: string; + fields: StyleEditorFieldSchema[]; +} + +/** + * Complete normalized form schema sent to UVE. + * This is the output format after processing a style editor form definition. + */ +export interface StyleEditorFormSchema { + contentType: string; + sections: StyleEditorSectionSchema[]; +} diff --git a/core-web/libs/sdk/uve/src/index.ts b/core-web/libs/sdk/uve/src/index.ts index 1b488c1a64be..12e1b01dbe1e 100644 --- a/core-web/libs/sdk/uve/src/index.ts +++ b/core-web/libs/sdk/uve/src/index.ts @@ -1,6 +1,2 @@ export * from './lib/core/core.utils'; export * from './lib/editor/public'; - -// Style Editor -export * from './lib/style-editor/types'; -export * from './lib/style-editor/public'; diff --git a/core-web/libs/sdk/uve/src/internal.ts b/core-web/libs/sdk/uve/src/internal.ts index aca1335774ec..375e25296462 100644 --- a/core-web/libs/sdk/uve/src/internal.ts +++ b/core-web/libs/sdk/uve/src/internal.ts @@ -3,3 +3,12 @@ export * from './lib/core/core.utils'; export * from './lib/dom/document-height-observer'; export * from './lib/dom/dom.utils'; export * from './lib/editor/internal'; + +// Style Editor — internal only +export { + defineStyleEditorSchema, + normalizeForm, + registerStyleEditorSchemas +} from './lib/style-editor/internal'; +export * from './lib/style-editor/public'; +export * from './lib/style-editor/types'; diff --git a/core-web/libs/sdk/uve/src/lib/style-editor/internal.ts b/core-web/libs/sdk/uve/src/lib/style-editor/internal.ts index ffc4c7db8290..a041b29beaeb 100644 --- a/core-web/libs/sdk/uve/src/lib/style-editor/internal.ts +++ b/core-web/libs/sdk/uve/src/lib/style-editor/internal.ts @@ -1,13 +1,16 @@ -import { +import { DotCMSUVEAction, UVE_MODE } from '@dotcms/types'; +import type { StyleEditorCheckboxOption, - StyleEditorField, StyleEditorFieldInputType, StyleEditorFieldSchema, - StyleEditorForm, StyleEditorFormSchema, - StyleEditorSection, StyleEditorSectionSchema -} from './types'; +} from '@dotcms/types/internal'; + +import { StyleEditorField, StyleEditorForm, StyleEditorSection } from './types'; + +import { getUVEState } from '../core/core.utils'; +import { sendMessageToUVE } from '../editor/public'; /** * Normalizes a field definition into the schema format expected by UVE. @@ -224,3 +227,46 @@ export function normalizeForm(form: StyleEditorForm): StyleEditorFormSchema { sections: form.sections.map(normalizeSection) }; } + +/** + * Normalizes and validates a style editor form definition into the schema format + * expected by UVE. Used internally by the schema builder. + * + * @internal + */ +export function defineStyleEditorSchema(form: StyleEditorForm): StyleEditorFormSchema { + return normalizeForm(form); +} + +/** + * Registers style editor form schemas with the UVE editor. + * + * Sends normalized style editor schemas to UVE for registration. + * Only registers schemas when UVE is in EDIT mode. + * + * @internal — called automatically by useEditableDotCMSPage and DotCMSEditablePageService + */ +export function registerStyleEditorSchemas(schemas: StyleEditorFormSchema[]): void { + const { mode } = getUVEState() || {}; + + if (!mode || mode !== UVE_MODE.EDIT) { + return; + } + + const validatedSchemas = schemas.filter((schema, index) => { + if (!schema.contentType) { + console.warn( + `[registerStyleEditorSchemas] Skipping schema with index [${index}] for not having a contentType` + ); + return false; + } + return true; + }); + + sendMessageToUVE({ + action: DotCMSUVEAction.REGISTER_STYLE_SCHEMAS, + payload: { + schemas: validatedSchemas + } + }); +} diff --git a/core-web/libs/sdk/uve/src/lib/style-editor/public.spec.ts b/core-web/libs/sdk/uve/src/lib/style-editor/public.spec.ts index 99b60324d9bc..a3195e3c3f4b 100644 --- a/core-web/libs/sdk/uve/src/lib/style-editor/public.spec.ts +++ b/core-web/libs/sdk/uve/src/lib/style-editor/public.spec.ts @@ -1,15 +1,8 @@ -import { normalizeForm } from './internal'; -import { defineStyleEditorSchema, styleEditorField } from './public'; -import { StyleEditorForm, StyleEditorFormSchema } from './types'; - -// Mock the internal normalizeForm function -jest.mock('./internal', () => ({ - normalizeForm: jest.fn() -})); +import { styleEditorField } from './public'; describe('styleEditorField', () => { describe('input', () => { - it('should create an input field with number type and number defaultValue', () => { + it('should create an input field with number type', () => { const config = { id: 'font-size', label: 'Font Size', @@ -27,7 +20,7 @@ describe('styleEditorField', () => { expect(result.inputType).toBe('number'); }); - it('should create an input field with text type and string defaultValue', () => { + it('should create an input field with text type', () => { const config = { id: 'font-name', label: 'Font Name', @@ -45,22 +38,6 @@ describe('styleEditorField', () => { expect(result.inputType).toBe('text'); }); - it('should create an input field without defaultValue', () => { - const config = { - id: 'font-size', - label: 'Font Size', - inputType: 'number' as const, - placeholder: 'Enter font size' - }; - - const result = styleEditorField.input(config); - - expect(result).toEqual({ - type: 'input', - ...config - }); - }); - it('should create an input field without placeholder', () => { const config = { id: 'font-size', @@ -119,7 +96,7 @@ describe('styleEditorField', () => { }); describe('radio', () => { - it('should create a radio field with string options', () => { + it('should create a radio field with object options', () => { const config = { id: 'alignment', label: 'Alignment', @@ -137,29 +114,6 @@ describe('styleEditorField', () => { ...config }); expect(result.type).toBe('radio'); - expect(result.options).toEqual([ - { label: 'Left', value: 'left' }, - { label: 'Center', value: 'center' }, - { label: 'Right', value: 'right' } - ]); - }); - - it('should create a radio field with object options', () => { - const config = { - id: 'theme', - label: 'Theme', - options: [ - { label: 'Light', value: 'light' }, - { label: 'Dark', value: 'dark' } - ] - }; - - const result = styleEditorField.radio(config); - - expect(result.options).toEqual([ - { label: 'Light', value: 'light' }, - { label: 'Dark', value: 'dark' } - ]); }); it('should create a radio field with options including images', () => { @@ -185,52 +139,25 @@ describe('styleEditorField', () => { }); }); - it('should create a radio field with mixed string and object options', () => { + it('should preserve columns config', () => { const config = { - id: 'theme', - label: 'Theme', + id: 'layout', + label: 'Layout', + columns: 2 as const, options: [ - { - label: 'Light', - value: 'light', - imageURL: 'https://example.com/light-theme.png' - }, - { label: 'Dark', value: 'dark' } - ] - }; - - const result = styleEditorField.radio(config); - - expect(result.options).toHaveLength(2); - expect(result.options[0]).toHaveProperty('imageURL'); - expect(result.options[1]).toEqual({ label: 'Dark', value: 'dark' }); - }); - - it('should handle options with only imageURL', () => { - const config = { - id: 'theme', - label: 'Theme', - options: [ - { - label: 'Light', - value: 'light', - imageURL: 'https://example.com/light.png' - } + { label: 'Left', value: 'left' }, + { label: 'Right', value: 'right' } ] }; const result = styleEditorField.radio(config); - expect(result.options[0]).toEqual({ - label: 'Light', - value: 'light', - imageURL: 'https://example.com/light.png' - }); + expect(result.columns).toBe(2); }); }); describe('checkboxGroup', () => { - it('should create a checkbox group field with new option structure', () => { + it('should create a checkbox group field', () => { const config = { id: 'text-decoration', label: 'Text Decoration', @@ -252,212 +179,3 @@ describe('styleEditorField', () => { }); }); }); - -describe('defineStyleEditorSchema', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should normalize a form with single column section', () => { - const mockSchema: StyleEditorFormSchema = { - contentType: 'test-content-type', - sections: [ - { - title: 'Typography', - fields: [ - { - type: 'input', - id: 'font-size', - label: 'Font Size', - config: { - inputType: 'number' - } - } - ] - } - ] - }; - - (normalizeForm as jest.Mock).mockReturnValue(mockSchema); - - const form: StyleEditorForm = { - contentType: 'test-content-type', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.input({ - id: 'font-size', - label: 'Font Size', - inputType: 'number' - }) - ] - } - ] - }; - - const result = defineStyleEditorSchema(form); - - expect(normalizeForm).toHaveBeenCalledWith(form); - expect(result).toEqual(mockSchema); - }); - - it('should normalize a form with multiple sections', () => { - const mockSchema: StyleEditorFormSchema = { - contentType: 'test-content-type', - sections: [ - { - title: 'Typography', - - fields: [ - { - type: 'input', - id: 'font-size', - label: 'Font Size', - config: { inputType: 'number' } - } - ] - }, - { - title: 'Colors', - - fields: [ - { - type: 'dropdown', - id: 'primary-color', - label: 'Primary Color', - config: { options: [{ label: 'Red', value: 'red' }] } - } - ] - } - ] - }; - - (normalizeForm as jest.Mock).mockReturnValue(mockSchema); - - const form: StyleEditorForm = { - contentType: 'test-content-type', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.input({ - id: 'font-size', - label: 'Font Size', - inputType: 'number' - }) - ] - }, - { - title: 'Colors', - fields: [ - styleEditorField.dropdown({ - id: 'primary-color', - label: 'Primary Color', - options: [{ label: 'Red', value: 'red' }] - }) - ] - } - ] - }; - - const result = defineStyleEditorSchema(form); - - expect(normalizeForm).toHaveBeenCalledWith(form); - expect(result).toEqual(mockSchema); - }); - - it('should normalize a form with multi-column section', () => { - const mockSchema: StyleEditorFormSchema = { - contentType: 'test-content-type', - sections: [ - { - title: 'Layout', - - fields: [ - { - type: 'input', - id: 'width', - label: 'Width', - config: { inputType: 'number' } - }, - - { - type: 'input', - id: 'height', - label: 'Height', - config: { inputType: 'number' } - } - ] - } - ] - }; - - (normalizeForm as jest.Mock).mockReturnValue(mockSchema); - - const form: StyleEditorForm = { - contentType: 'test-content-type', - sections: [ - { - title: 'Layout', - - fields: [ - styleEditorField.input({ - id: 'width', - label: 'Width', - inputType: 'number' - }), - - styleEditorField.input({ - id: 'height', - label: 'Height', - inputType: 'number' - }) - ] - } - ] - }; - - const result = defineStyleEditorSchema(form); - - expect(normalizeForm).toHaveBeenCalledWith(form); - expect(result).toEqual(mockSchema); - }); - - it('should handle empty sections array', () => { - const mockSchema: StyleEditorFormSchema = { - contentType: 'test-content-type', - sections: [] - }; - - (normalizeForm as jest.Mock).mockReturnValue(mockSchema); - - const form: StyleEditorForm = { - contentType: 'test-content-type', - sections: [] - }; - - const result = defineStyleEditorSchema(form); - - expect(normalizeForm).toHaveBeenCalledWith(form); - expect(result).toEqual(mockSchema); - }); - - it('should preserve contentType in the result', () => { - const mockSchema: StyleEditorFormSchema = { - contentType: 'custom-content-type', - sections: [] - }; - - (normalizeForm as jest.Mock).mockReturnValue(mockSchema); - - const form: StyleEditorForm = { - contentType: 'custom-content-type', - sections: [] - }; - - const result = defineStyleEditorSchema(form); - - expect(result.contentType).toBe('custom-content-type'); - }); -}); diff --git a/core-web/libs/sdk/uve/src/lib/style-editor/public.ts b/core-web/libs/sdk/uve/src/lib/style-editor/public.ts index 404f77a0ca96..4fcdc922f55f 100644 --- a/core-web/libs/sdk/uve/src/lib/style-editor/public.ts +++ b/core-web/libs/sdk/uve/src/lib/style-editor/public.ts @@ -1,105 +1,28 @@ -import { DotCMSUVEAction, UVE_MODE } from '@dotcms/types'; +import type { + StyleEditorFieldInputType, + StyleEditorOption, + StyleEditorRadioOption +} from '@dotcms/types/internal'; -import { normalizeForm } from './internal'; import { StyleEditorCheckboxGroupField, StyleEditorDropdownField, - StyleEditorFieldInputType, - StyleEditorForm, - StyleEditorFormSchema, StyleEditorInputField, StyleEditorInputFieldConfig, - StyleEditorOption, - StyleEditorRadioField, - StyleEditorRadioOption + StyleEditorRadioField } from './types'; -import { getUVEState } from '../core/core.utils'; -import { sendMessageToUVE } from '../editor/public'; - /** * Helper functions for creating style editor field definitions. - * - * Provides type-safe factory functions for creating different types of form fields - * used in the style editor. Each function creates a field definition with the - * appropriate `type` property automatically set, eliminating the need to manually - * specify the type discriminator. - * - * **Available Field Types:** - * - `input`: Text or number input fields - * - `dropdown`: Single-value selection from a dropdown list - * - `radio`: Single-value selection from radio button options (supports visual options with images) - * - `checkboxGroup`: Multiple-value selection from checkbox options - * - * These factory functions ensure type safety by inferring the correct field type - * based on the configuration provided, and they automatically set the `type` property - * to match the factory function used. + * Used by the dotCMS schema builder UI. * * @experimental This API is experimental and may be subject to change. - * - * @example - * ```typescript - * const form = defineStyleEditorSchema({ - * contentType: 'my-content-type', - * sections: [ - * { - * title: 'Typography', - * fields: [ - * styleEditorField.input({ - * id: 'font-size', - * label: 'Font Size', - * inputType: 'number' - * }), - * styleEditorField.dropdown({ - * id: 'font-family', - * label: 'Font Family', - * options: ['Arial', 'Helvetica'] - * }), - * styleEditorField.radio({ - * id: 'alignment', - * label: 'Alignment', - * options: ['Left', 'Center', 'Right'] - * }) - * ] - * } - * ] - * }); - * ``` */ export const styleEditorField = { /** * Creates an input field definition. * - * Supports both text and number input types for different value types. - * * @experimental This method is experimental and may be subject to change. - * - * @typeParam T - The input type ('text' or 'number'), inferred from `config.inputType` - * @param config - Input field configuration - * @param config.id - The unique identifier for this field - * @param config.label - The label displayed for this input field - * @param config.inputType - The type of input ('text' or 'number') - * @param config.placeholder - Optional placeholder text for the input - * @returns A complete input field definition with type 'input' - * - * @example - * ```typescript - * // Number input - * styleEditorField.input({ - * id: 'font-size', - * label: 'Font Size', - * inputType: 'number', - * placeholder: 'Enter font size' - * }) - * - * // Text input - * styleEditorField.input({ - * id: 'font-name', - * label: 'Font Name', - * inputType: 'text', - * placeholder: 'Enter font name' - * }) - * ``` */ input: ( config: StyleEditorInputFieldConfig @@ -112,52 +35,7 @@ export const styleEditorField = { /** * Creates a dropdown field definition. * - * Allows users to select a single value from a list of options. - * Options can be provided as simple strings or as objects with label and value. - * - * **Best Practice:** Use `as const` when defining options for better type safety: - * ```typescript - * const OPTIONS = [ - * { label: '18', value: '18px' }, - * { label: '24', value: '24px' } - * ] as const; - * - * styleEditorField.dropdown({ - * id: 'size', - * label: 'Size', - * options: OPTIONS - * }); - * ``` - * * @experimental This method is experimental and may be subject to change. - * - * @param config - Dropdown field configuration (without the 'type' property) - * @param config.id - The unique identifier for this field - * @param config.label - The label displayed for this dropdown field - * @param config.options - Array of options. Can be strings or objects with label and value. Use `as const` for best type safety. - * @returns A complete dropdown field definition with type 'dropdown' - * - * @example - * ```typescript - * // Simple string options - * styleEditorField.dropdown({ - * id: 'font-family', - * label: 'Font Family', - * options: ['Arial', 'Helvetica', 'Times New Roman'] - * }) - * - * // Object options with custom labels (recommended: use 'as const') - * const OPTIONS = [ - * { label: 'Light Theme', value: 'light' }, - * { label: 'Dark Theme', value: 'dark' } - * ] as const; - * - * styleEditorField.dropdown({ - * id: 'theme', - * label: 'Theme', - * options: OPTIONS - * }) - * ``` */ dropdown: (config: Omit): StyleEditorDropdownField => ({ type: 'dropdown', @@ -168,69 +46,7 @@ export const styleEditorField = { /** * Creates a radio button field definition. * - * Allows users to select a single option from a list. Supports visual - * options with background images for enhanced UI. Options can be provided - * as simple strings or as objects with label, value, and optional image properties. - * - * **Layout Options:** - * - `columns: 1` (default): Single column list layout - * - `columns: 2`: Two-column grid layout, ideal for visual options with images - * - * **Best Practice:** Use `as const` when defining options for better type safety: - * ```typescript - * const RADIO_OPTIONS = [ - * { label: 'Left', value: 'left' }, - * { label: 'Right', value: 'right' } - * ] as const; - * - * styleEditorField.radio({ - * id: 'layout', - * label: 'Layout', - * options: RADIO_OPTIONS - * }); - * ``` - * * @experimental This method is experimental and may be subject to change. - * - * @param config - Radio field configuration (without the 'type' property) - * @param config.id - The unique identifier for this field - * @param config.label - The label displayed for this radio group - * @param config.options - Array of options. Can be strings or objects with label, value, and optional image properties. Use `as const` for best type safety. - * @param config.columns - Optional number of columns (1 or 2). Defaults to 1 (single column) - * @returns A complete radio field definition with type 'radio' - * - * @example - * ```typescript - * // Simple string options (single column) - * styleEditorField.radio({ - * id: 'alignment', - * label: 'Alignment', - * options: ['Left', 'Center', 'Right'] - * }) - * - * // Two-column grid layout with images (recommended: use 'as const') - * const LAYOUT_OPTIONS = [ - * { - * label: 'Left', - * value: 'left', - * imageURL: 'https://example.com/layout-left.png', - * }, - * { - * label: 'Right', - * value: 'right', - * imageURL: 'https://example.com/layout-right.png', - * }, - * { label: 'Center', value: 'center' }, - * { label: 'Overlap', value: 'overlap' } - * ] as const; - * - * styleEditorField.radio({ - * id: 'layout', - * label: 'Layout', - * columns: 2, - * options: LAYOUT_OPTIONS - * }) - * ``` */ radio: (config: Omit): StyleEditorRadioField => ({ type: 'radio', @@ -241,52 +57,7 @@ export const styleEditorField = { /** * Creates a checkbox group field definition. * - * Allows users to select multiple options simultaneously. Each option - * can be independently checked or unchecked. The default checked state - * is defined directly in each option's `value` property (boolean). - * - * **Key Differences from Other Field Types:** - * - Uses `key` instead of `value` for the identifier (to avoid confusion) - * - Checked state is managed by the form system, not stored in the option definition - * - * **Why `key` instead of `value`?** - * In dropdown and radio fields, `value` represents the actual selected value (string). - * In checkbox groups, the actual value is boolean (checked/unchecked), so we use - * `key` for the identifier to avoid confusion. This makes it clear that `value` - * is the boolean state, not just an identifier. - * * @experimental This method is experimental and may be subject to change. - * - * @param config - Checkbox group field configuration (without the 'type' property) - * @param config.id - The unique identifier for this field - * @param config.label - The label displayed for this checkbox group - * @param config.options - Array of checkbox options with label and key - * @returns A complete checkbox group field definition with type 'checkboxGroup' - * - * @example - * ```typescript - * styleEditorField.checkboxGroup({ - * id: 'text-decoration', - * label: 'Text Decoration', - * options: [ - * { label: 'Underline', key: 'underline' }, - * { label: 'Overline', key: 'overline' }, - * { label: 'Line Through', key: 'line-through' } - * ] - * }) - * - * // Example with type settings - * styleEditorField.checkboxGroup({ - * id: 'type-settings', - * label: 'Type settings', - * options: [ - * { label: 'Bold', key: 'bold' }, - * { label: 'Italic', key: 'italic' }, - * { label: 'Underline', key: 'underline' }, - * { label: 'Strikethrough', key: 'strikethrough' } - * ] - * }) - * ``` */ checkboxGroup: ( config: Omit @@ -295,145 +66,3 @@ export const styleEditorField = { ...config }) }; - -/** - * Normalizes and validates a style editor form definition. - * - * Converts the developer-friendly form structure into the schema format - * expected by UVE (Universal Visual Editor). This function processes the - * form definition and transforms it into the normalized schema format where: - * - * - All field-specific properties are moved into `config` objects - * - String options are normalized to `{ label, value }` objects - * - Sections are organized into the multi-dimensional array structure required by UVE - * - * The normalization process ensures consistency and type safety in the schema - * format sent to UVE. After normalization, use `registerStyleEditorSchemas` - * to register the schema with the UVE editor. - * - * @experimental This method is experimental and may be subject to change. - * - * @param form - The style editor form definition containing contentType and sections - * @param form.contentType - The content type identifier for this form - * @param form.sections - Array of sections, each containing a title and fields array - * @returns The normalized form schema ready to be sent to UVE - * - * @example - * ```typescript - * const formSchema = defineStyleEditorSchema({ - * contentType: 'my-content-type', - * sections: [ - * { - * title: 'Typography', - * fields: [ - * styleEditorField.input({ - * id: 'font-size', - * label: 'Font Size', - * inputType: 'number' - * }), - * styleEditorField.dropdown({ - * id: 'font-family', - * label: 'Font Family', - * options: ['Arial', 'Helvetica'] - * }) - * ] - * }, - * { - * title: 'Colors', - * fields: [ - * styleEditorField.input({ - * id: 'primary-color', - * label: 'Primary Color', - * inputType: 'text' - * }), - * styleEditorField.input({ - * id: 'secondary-color', - * label: 'Secondary Color', - * inputType: 'text' - * }) - * ] - * } - * ] - * }); - * - * // Register the schema with UVE - * registerStyleEditorSchemas([formSchema]); - * ``` - */ -export function defineStyleEditorSchema(form: StyleEditorForm): StyleEditorFormSchema { - return normalizeForm(form); -} - -/** - * Registers style editor form schemas with the UVE editor. - * - * Sends normalized style editor schemas to the UVE (Universal Visual Editor) - * for registration. The schemas must be normalized using `defineStyleEditorSchema` - * before being passed to this function. - * - * **Behavior:** - * - Only registers schemas when UVE is in EDIT mode - * - Validates that each schema has a `contentType` property - * - Skips schemas without `contentType` and logs a warning - * - Sends the validated schemas to UVE via the `REGISTER_STYLE_SCHEMAS` action - * - * **Note:** This function will silently return early if UVE is not in EDIT mode, - * so it's safe to call even when the editor is not active. - * - * @experimental This method is experimental and may be subject to change. - * - * @param schemas - Array of normalized style editor form schemas to register with UVE - * @returns void - This function does not return a value - * - * @example - * ```typescript - * // Create and normalize a form schema - * const formSchema = defineStyleEditorSchema({ - * contentType: 'my-content-type', - * sections: [ - * { - * title: 'Typography', - * fields: [ - * styleEditorField.input({ - * id: 'font-size', - * label: 'Font Size', - * inputType: 'number' - * }) - * ] - * } - * ] - * }); - * - * // Register the schema with UVE - * registerStyleEditorSchemas([formSchema]); - * - * // Register multiple schemas at once - * const schema1 = defineStyleEditorSchema({ ... }); - * const schema2 = defineStyleEditorSchema({ ... }); - * registerStyleEditorSchemas([schema1, schema2]); - * ``` - */ -export function registerStyleEditorSchemas(schemas: StyleEditorFormSchema[]): void { - const { mode } = getUVEState() || {}; - - if (!mode || mode !== UVE_MODE.EDIT) { - return; - } - - const validatedSchemas = schemas.filter((schema, index) => { - if (!schema.contentType) { - console.warn( - `[registerStyleEditorSchemas] Skipping schema with index [${index}] for not having a contentType` - ); - return false; - } - return true; - }); - - sendMessageToUVE({ - action: DotCMSUVEAction.REGISTER_STYLE_SCHEMAS, - payload: { - schemas: validatedSchemas - } - }); -} diff --git a/core-web/libs/sdk/uve/src/lib/style-editor/types.ts b/core-web/libs/sdk/uve/src/lib/style-editor/types.ts index cd87afe6a1a5..b81dd9902f46 100644 --- a/core-web/libs/sdk/uve/src/lib/style-editor/types.ts +++ b/core-web/libs/sdk/uve/src/lib/style-editor/types.ts @@ -1,121 +1,24 @@ -/** - * Available field types for the style editor. - * - * Each field type represents a different input control that can be used - * in the style editor form. Currently supported field types: - * - `input`: Text or number input fields - * - `dropdown`: Single-value selection from a dropdown list - * - `radio`: Single-value selection from radio button options - * - `checkboxGroup`: Multiple-value selection from checkbox options - * - `switch`: Boolean toggle switch (reserved for future implementation) - */ -export type StyleEditorFieldType = 'input' | 'dropdown' | 'radio' | 'checkboxGroup'; - -/** - * Available input types for input fields in the style editor. - * - * Determines the type of input control and the expected value type: - * - `'text'`: Standard text input for string values (e.g., font names, color codes) - * - `'number'`: Numeric input for number values (e.g., font sizes, dimensions) - */ -export type StyleEditorFieldInputType = 'text' | 'number'; +import type { + StyleEditorCheckboxOption, + StyleEditorFieldType, + StyleEditorOption, + StyleEditorRadioOption +} from '@dotcms/types/internal'; /** * Configuration type for creating input fields. * - * This type is used by `styleEditorField.input()` to provide compile-time - * type checking for input field definitions. - * * @typeParam T - The input type ('text' or 'number') */ -export interface StyleEditorInputFieldConfig { - /** The unique identifier for this field */ +export interface StyleEditorInputFieldConfig { id: string; - /** The label text displayed to users for this field */ label: string; - /** The input type ('text' for strings, 'number' for numbers) */ inputType: T; - /** Optional placeholder text shown when the input is empty */ placeholder?: string; } -/** - * Base option object with label and value properties. - * - * Used in dropdown, radio, and checkbox group fields to define - * selectable options with separate display labels and values. - * - * @property label - Display label shown to users - * @property value - Value returned when this option is selected - */ -export interface StyleEditorOptionObject { - /** Display label shown to users */ - label: string; - /** Value returned when this option is selected */ - value: string; -} - -/** - * Extended option object for radio fields with visual properties. - * - * Extends the base option with optional image properties for - * creating visual radio button options (e.g., layout selectors with preview images). - * - * @property label - Display label shown to users - * @property value - Value returned when this option is selected - * @property imageURL - Optional URL to an image displayed for this option - */ -export interface StyleEditorRadioOptionObject extends StyleEditorOptionObject { - /** Optional URL to an image displayed for this option */ - imageURL?: string; -} - -/** - * Option type for dropdown and checkbox group fields. - * - * Can be a simple string (used as both label and value) - * or an object with separate label and value properties. - * - * @example - * ```typescript - * // String option - 'Arial' is used as both label and value - * const stringOption: StyleEditorOption = 'Arial'; - * - * // Object option - separate label and value - * const objectOption: StyleEditorOption = { - * label: 'Times New Roman', - * value: 'times' - * }; - * ``` - */ -export type StyleEditorOption = StyleEditorOptionObject; - /** * Helper type that extracts the union of all option values from an array of options. - * - * This type extracts the `value` property from each option object and creates - * a union type of all possible values. - * - * **Note:** For full type safety and autocomplete, use `as const` when defining options: - * ```typescript - * const options = [ - * { label: 'The one', value: 'one' }, - * { label: 'The two', value: 'two' } - * ] as const; - * ``` - * - * @typeParam T - Array of option objects (should be `as const` for best results) - * - * @example - * ```typescript - * const options = [ - * { label: 'The one', value: 'one' }, - * { label: 'The two', value: 'two' } - * ] as const; - * - * type OptionValues = StyleEditorOptionValues; - * // Result: 'one' | 'two' - * ``` */ export type StyleEditorOptionValues = T[number] extends { value: infer V; @@ -125,31 +28,6 @@ export type StyleEditorOptionValues = T[ /** * Helper type that extracts the union of all radio option values from an array of radio options. - * - * Similar to `StyleEditorOptionValues`, but handles radio options which can be - * strings or objects. Extracts the `value` property from option objects, or uses - * the string itself if the option is a string. - * - * **Note:** For full type safety and autocomplete, use `as const` when defining options: - * ```typescript - * const options = [ - * { label: 'The one', value: 'one' }, - * { label: 'The two', value: 'two' } - * ] as const; - * ``` - * - * @typeParam T - Array of radio option objects or strings (should be `as const` for best results) - * - * @example - * ```typescript - * const options = [ - * { label: 'The one', value: 'one' }, - * { label: 'The two', value: 'two' } - * ] as const; - * - * type RadioOptionValues = StyleEditorRadioOptionValues; - * // Result: 'one' | 'two' - * ``` */ export type StyleEditorRadioOptionValues = T[number] extends infer U @@ -161,335 +39,61 @@ export type StyleEditorRadioOptionValues; /** - * Base field definition that all field types extend. - * - * Provides the common properties shared by all field types in the style editor. - * All specific field types must include these base properties. The `type` property - * serves as a discriminator that allows TypeScript to narrow union types based on - * the field type. - * - * @property type - The type of field (discriminator for union types, enables type narrowing) - * @property label - The human-readable label displayed for this field in the UI + * Base field definition shared by all field types. */ export interface StyleEditorBaseField { - /** The unique identifier for this field */ id: string; - /** The type of field, used to discriminate between different field types in union types */ type: StyleEditorFieldType; - /** The label text displayed to users for this field */ label: string; } /** * Input field definition. - * - * Supports both text and number input types for different value types. - * - * @example - * ```typescript - * // Number input - * const numberField: StyleEditorInputField = { - * type: 'input', - * id: 'font-size', - * label: 'Font Size', - * inputType: 'number', - * placeholder: 'Enter font size' - * }; - * - * // Text input - * const textField: StyleEditorInputField = { - * type: 'input', - * id: 'font-name', - * label: 'Font Name', - * inputType: 'text', - * placeholder: 'Enter font name' - * }; - * ``` */ export type StyleEditorInputField = | (StyleEditorBaseField & { - /** Discriminator: must be 'input' */ type: 'input'; - /** Input type for number values */ inputType: 'number'; - /** Optional placeholder text shown when the input is empty */ placeholder?: string; }) | (StyleEditorBaseField & { - /** Discriminator: must be 'input' */ type: 'input'; - /** Input type for text/string values */ inputType: 'text'; - /** Optional placeholder text shown when the input is empty */ placeholder?: string; }); /** * Dropdown field definition for single-value selection. - * - * Allows users to select a single option from a dropdown list. - * Options can be provided as simple strings or as objects with - * separate label and value properties for more flexibility. - * - * **Best Practice:** Use `as const` when defining options for better type safety: - * ```typescript - * const OPTIONS = [ - * { label: '18', value: '18px' }, - * { label: '24', value: '24px' } - * ] as const; - * - * styleEditorField.dropdown({ - * id: 'size', - * label: 'Size', - * options: OPTIONS - * }); - * ``` - * - * @property type - Must be 'dropdown' - * @property options - Array of selectable options. Can be strings or objects with label/value. Use `as const` for best type safety. - * - * @example - * ```typescript - * const dropdownField: StyleEditorDropdownField = { - * type: 'dropdown', - * id: 'font-family', - * label: 'Font Family', - * options: ['Arial', 'Helvetica', { label: 'Times New Roman', value: 'times' }] - * }; - * ``` */ export interface StyleEditorDropdownField extends StyleEditorBaseField { - /** Discriminator: must be 'dropdown' */ type: 'dropdown'; - /** Array of selectable options. Can be strings or objects with label and value properties. Accepts readonly arrays (use `as const` for best type safety). */ options: readonly StyleEditorOption[]; } /** - * Radio button field definition for single-value selection with visual options. - * - * Allows users to select a single option from a radio button group. - * Supports visual options with background images for enhanced UI/UX. - * Options can be provided as simple strings or as objects with label, - * value, and optional image properties. - * - * **Layout Options:** - * - `columns: 1` (default): Single column list layout - * - `columns: 2`: Two-column grid layout, ideal for visual options with images - * - * **Best Practice:** Use `as const` when defining options for better type safety: - * ```typescript - * const RADIO_OPTIONS = [ - * { label: 'Left', value: 'left' }, - * { label: 'Right', value: 'right' } - * ] as const; - * - * styleEditorField.radio({ - * id: 'layout', - * label: 'Layout', - * options: RADIO_OPTIONS - * }); - * ``` - * - * @property type - Must be 'radio' - * @property options - Array of selectable options. Can be strings or objects with label, value, and optional image properties. Use `as const` for best type safety. - * @property columns - Optional number of columns for layout (1 or 2). Defaults to 1 - * - * @example - * ```typescript - * // Single column layout (default) - * const alignmentField: StyleEditorRadioField = { - * type: 'radio', - * id: 'alignment', - * label: 'Alignment', - * options: ['Left', 'Center', 'Right'] - * }; - * - * // Two-column grid layout with images - * const layoutField: StyleEditorRadioField = { - * type: 'radio', - * id: 'layout', - * label: 'Layout', - * columns: 2, - * options: [ - * { - * label: 'Left', - * value: 'left', - * imageURL: 'https://example.com/layout-left.png' - * }, - * { - * label: 'Right', - * value: 'right', - * imageURL: 'https://example.com/layout-right.png' - * }, - * { label: 'Center', value: 'center' }, - * { label: 'Overlap', value: 'overlap' } - * ] - * }; - * ``` + * Radio button field definition for single-value selection with optional visual options. */ export interface StyleEditorRadioField extends StyleEditorBaseField { - /** Discriminator: must be 'radio' */ type: 'radio'; - /** - * Array of selectable options. Can be: - * - Simple strings (used as both label and value) - * - Objects with label, value, and optional imageURL for visual options - * Accepts readonly arrays (use `as const` for best type safety). - */ options: readonly StyleEditorRadioOption[]; - /** - * Number of columns to display options in. - * - `1`: Single column list layout (default) - * - `2`: Two-column grid layout - */ columns?: 1 | 2; } /** * Checkbox group field definition for multiple-value selection. - * - * Allows users to select multiple options simultaneously. Each option - * can be independently checked or unchecked. The checked state is managed - * by the form system and is not stored in the option definition. - * - * **Key Differences from Other Field Types:** - * - Uses `key` instead of `value` for the identifier (to avoid confusion) - * - Checked state is managed by the form system, not stored in the option definition - * - * @property type - Must be 'checkboxGroup' - * @property options - Array of checkbox options with label and key - * - * @example - * ```typescript - * const checkboxField: StyleEditorCheckboxGroupField = { - * type: 'checkboxGroup', - * id: 'text-decoration', - * label: 'Text Decoration', - * options: [ - * { label: 'Underline', key: 'underline' }, - * { label: 'Overline', key: 'overline' }, - * { label: 'Line Through', key: 'line-through' } - * ] - * }; - * ``` */ export interface StyleEditorCheckboxGroupField extends StyleEditorBaseField { - /** Discriminator: must be 'checkboxGroup' */ type: 'checkboxGroup'; - /** - * Array of checkbox options. Each option contains: - * - `label`: Display text shown to users - * - `key`: Unique identifier for this checkbox option - */ options: StyleEditorCheckboxOption[]; } /** * Union type of all possible field definitions. - * - * Represents any valid field type that can be used in a style editor form. - * This is a discriminated union type, meaning TypeScript can narrow the type - * based on the `type` property. Use this type when you need to work with - * fields of unknown or mixed types. - * - * **Supported Field Types:** - * - `StyleEditorInputField`: Text or number input fields - * - `StyleEditorDropdownField`: Single-value selection from dropdown - * - `StyleEditorRadioField`: Single-value selection from radio buttons - * - `StyleEditorCheckboxGroupField`: Multiple-value selection from checkboxes - * - * **Note:** The `switch` field type is reserved for future implementation - * and is not currently included in this union type. - * - * @example - * ```typescript - * const fields: StyleEditorField[] = [ - * { type: 'input', id: 'font-size', label: 'Font Size', inputType: 'number' }, - * { type: 'dropdown', id: 'font-family', label: 'Font Family', options: ['Arial', 'Helvetica'] }, - * { type: 'radio', id: 'theme', label: 'Theme', options: ['Light', 'Dark'] }, - * { - * type: 'checkboxGroup', - * id: 'styles', - * label: 'Styles', - * options: [ - * { label: 'Bold', key: 'bold', value: true }, - * { label: 'Italic', key: 'italic', value: false } - * ] - * } - * ]; - * - * // TypeScript can narrow the type based on the discriminator - * function processField(field: StyleEditorField) { - * if (field.type === 'input') { - * // field is now narrowed to StyleEditorInputField - * console.log(field.inputType); // Type-safe access - * } - * } - * ``` */ export type StyleEditorField = | StyleEditorInputField @@ -499,240 +103,16 @@ export type StyleEditorField = /** * Section definition for organizing fields in a style editor form. - * - * Sections group related fields together with a title. All sections use a - * single-column layout with a flat array of fields. During normalization, - * these fields are automatically organized into the multi-dimensional array - * structure required by UVE. - * - * @property title - The section title displayed to users - * @property fields - Array of field definitions in this section - * - * @example - * ```typescript - * const section: StyleEditorSection = { - * title: 'Typography', - * fields: [ - * { type: 'input', id: 'font-size', label: 'Font Size', inputType: 'number' }, - * { type: 'dropdown', id: 'font-family', label: 'Font Family', options: ['Arial', 'Helvetica'] }, - * { type: 'radio', id: 'alignment', label: 'Alignment', options: ['Left', 'Center', 'Right'] } - * ] - * }; - * ``` */ export interface StyleEditorSection { - /** The section title displayed to users */ title: string; - /** Array of field definitions in this section */ fields: StyleEditorField[]; } /** - * Complete style editor form definition. - * - * Represents the full structure of a style editor form, including - * the content type identifier and all sections with their fields. - * This is the developer-friendly format used to define forms before - * they are normalized and sent to UVE. - * - * **Form Structure:** - * - Each form is associated with a specific content type via `contentType` - * - Forms contain one or more sections, each with a title and array of fields - * - All sections use a single-column layout (flat array of fields) - * - During normalization via `defineStyleEditorForm`, sections are automatically - * converted to the multi-dimensional array structure required by UVE - * - * @property contentType - The content type identifier this form is associated with - * @property sections - Array of sections, each containing a title and fields array - * - * @example - * ```typescript - * const form: StyleEditorForm = { - * contentType: 'my-content-type', - * sections: [ - * { - * title: 'Typography', - * fields: [ - * { type: 'input', id: 'font-size', label: 'Font Size', inputType: 'number' }, - * { type: 'dropdown', id: 'font-family', label: 'Font Family', options: ['Arial', 'Helvetica'] }, - * { type: 'radio', id: 'alignment', label: 'Alignment', options: ['Left', 'Center', 'Right'] } - * ] - * }, - * { - * title: 'Colors', - * fields: [ - * { type: 'input', id: 'primary-color', label: 'Primary Color', inputType: 'text' }, - * { type: 'input', id: 'secondary-color', label: 'Secondary Color', inputType: 'text' } - * ] - * } - * ] - * }; - * ``` + * Complete style editor form definition (developer-facing input format). */ export interface StyleEditorForm { - /** The content type identifier this form is associated with */ contentType: string; - /** Array of sections, each containing a title and fields */ sections: StyleEditorSection[]; } - -/** - * ============================================================================ - * UVE Style Editor Schema Types - * ============================================================================ - * - * The following types represent the normalized schema format sent to UVE - * (Universal Visual Editor). These are the output types after processing - * and normalizing the developer-friendly StyleEditorForm definitions. - * ============================================================================ - */ - -/** - * Configuration object for normalized field schemas. - * - * Contains all possible field-specific properties after normalization. - * This interface is used by `StyleEditorFieldSchema` to define the `config` property. - * - * **Note:** All properties are optional since different field types use different subsets: - * - Input fields use: `inputType`, `placeholder` - * - Dropdown fields use: `options` - * - Radio fields use: `options`, `columns` - * - Checkbox fields use: `options` - */ -export interface StyleEditorFieldSchemaConfig { - /** Optional input type for input fields ('text' or 'number') */ - inputType?: StyleEditorFieldInputType; - /** Optional placeholder text shown when the field is empty */ - placeholder?: string; - /** - * Optional array of normalized options for dropdown, radio, and checkbox fields. - * In the normalized schema, options are always in object form (strings are converted). - * Uses `StyleEditorRadioOptionObject` as the superset that supports all option properties. - */ - options?: StyleEditorRadioOptionObject[]; - /** - * Number of columns to display options in (for radio fields). - * - `1`: Single column list layout (default) - * - `2`: Two-column grid layout - */ - columns?: 1 | 2; -} - -/** - * Normalized field schema sent to UVE. - * - * This is the transformed format of field definitions after normalization. - * All field-specific properties are moved into a `config` object, ensuring - * a consistent structure that UVE can consume. - * - * **Normalization Process:** - * - Type-specific properties (like `inputType`, `options`, `placeholder`) are moved into `config` - * - String options are normalized to `{ label, value }` objects - * - Radio field image properties (`imageURL`) are preserved in option objects - * - The `type`, `id`, and `label` remain at the top level for easy access - * - * @property type - The field type identifier (discriminator for field types) - * @property label - The human-readable label displayed for this field - * @property config - Object containing all field-specific configuration properties - */ -export interface StyleEditorFieldSchema { - /** The unique identifier for this field */ - id: string; - /** The field type identifier */ - type: StyleEditorFieldType; - /** The field label */ - label: string; - /** Object containing all field-specific configuration */ - config: StyleEditorFieldSchemaConfig; -} - -/** - * Normalized section schema sent to UVE. - * - * Represents a section in the normalized UVE format. All sections - * use a consistent multi-dimensional array structure (array of arrays), - * even for single-column layouts. This ensures a uniform schema format - * that UVE can consume. - * - * **Structure:** - * - The `fields` property is always a two-dimensional array - * - Each inner array represents a column (currently all sections use a single column) - * - Each field in the inner arrays is a normalized `StyleEditorFieldSchema` with - * all properties moved into the `config` object - * - * @property title - The section title displayed to users - * @property fields - Two-dimensional array where each inner array contains normalized field schemas for a column - * - * @example - * ```typescript - * // Single-column section (normalized format) - * const sectionSchema: StyleEditorSectionSchema = { - * title: 'Typography', - * fields: [ - * // Single column containing all fields - * [ - * { type: 'input', id: 'font-size', label: 'Font Size', config: { inputType: 'number' } }, - * { type: 'dropdown', id: 'font-family', label: 'Font Family', config: { options: [...] } } - * ] - * ] - * }; - * ``` - */ -export interface StyleEditorSectionSchema { - /** The section title displayed to users */ - title: string; - /** Two-dimensional array where each inner array contains normalized field schemas for a column */ - fields: StyleEditorFieldSchema[]; -} - -/** - * Complete normalized form schema sent to UVE. - * - * This is the final output format after normalizing a `StyleEditorForm` using - * `defineStyleEditorForm`. The form structure is transformed into a consistent - * schema format that UVE (Universal Visual Editor) can consume. - * - * **Normalization Characteristics:** - * - All sections use the multi-dimensional array structure (`fields: StyleEditorFieldSchema[][]`) - * - All field-specific properties are moved into `config` objects - * - String options are normalized to `{ label, value }` objects - * - The `contentType` identifier is preserved for association with content types - * - * This schema format is ready to be sent to UVE via `registerStyleEditorSchemas`. - * - * @property contentType - The content type identifier this form is associated with - * @property sections - Array of normalized section schemas, each with fields organized as a multi-dimensional array - * - * @example - * ```typescript - * const formSchema: StyleEditorFormSchema = { - * contentType: 'my-content-type', - * sections: [ - * { - * title: 'Typography', - * fields: [ - * // Single column containing all fields - * [ - * { type: 'input', id: 'font-size', label: 'Font Size', config: { inputType: 'number' } }, - * { type: 'dropdown', id: 'font-family', label: 'Font Family', config: { options: [{ label: 'Arial', value: 'Arial' }] } } - * ] - * ] - * }, - * { - * title: 'Colors', - * fields: [ - * [ - * { type: 'input', id: 'primary-color', label: 'Primary Color', config: { inputType: 'text' } } - * ] - * ] - * } - * ] - * }; - * ``` - */ -export interface StyleEditorFormSchema { - /** The content type identifier this form is associated with */ - contentType: string; - /** Array of normalized section schemas */ - sections: StyleEditorSectionSchema[]; -} diff --git a/core-web/libs/sdk/uve/src/script/sdk-editor.ts b/core-web/libs/sdk/uve/src/script/sdk-editor.ts index dde8754bda3f..46385e639ef9 100644 --- a/core-web/libs/sdk/uve/src/script/sdk-editor.ts +++ b/core-web/libs/sdk/uve/src/script/sdk-editor.ts @@ -18,7 +18,7 @@ import { reorderMenu, updateNavigation } from '../lib/editor/public'; -import { registerStyleEditorSchemas } from '../lib/style-editor/public'; +import { registerStyleEditorSchemas } from '../lib/style-editor/internal'; declare global { interface Window { diff --git a/examples/angular/src/app/dotcms/pages/page/page.ts b/examples/angular/src/app/dotcms/pages/page/page.ts index a7d7847fee91..64b669a1e71d 100644 --- a/examples/angular/src/app/dotcms/pages/page/page.ts +++ b/examples/angular/src/app/dotcms/pages/page/page.ts @@ -19,9 +19,7 @@ import { DynamicComponentEntity, } from '@dotcms/angular'; import { DotCMSComposedPageResponse, DotCMSPageAsset } from '@dotcms/types'; -import { registerStyleEditorSchemas } from '@dotcms/uve'; import { LoadingComponent } from '../../../components/loading/loading.component'; -import { ACTIVITY_SCHEMA, BANNER_SCHEMA } from '../../style-editor-schemas/schemas'; const DYNAMIC_COMPONENTS: { [key: string]: DynamicComponentEntity } = { @@ -76,8 +74,6 @@ export class PageComponent implements OnInit { private readonly client = inject(DotCMSClient); ngOnInit() { - this.#defineStyleEditorSchemas(); - const route = this.router.url.split('?')[0] || '/'; // Convert promise to observable and merge with editable page service @@ -102,8 +98,4 @@ export class PageComponent implements OnInit { }, }); } - - #defineStyleEditorSchemas() { - registerStyleEditorSchemas([ACTIVITY_SCHEMA, BANNER_SCHEMA]) - } } diff --git a/examples/angular/src/app/dotcms/style-editor-schemas/schemas.ts b/examples/angular/src/app/dotcms/style-editor-schemas/schemas.ts deleted file mode 100644 index d5b67006dab6..000000000000 --- a/examples/angular/src/app/dotcms/style-editor-schemas/schemas.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { defineStyleEditorSchema, styleEditorField } from "@dotcms/uve"; - -export const ACTIVITY_SCHEMA = defineStyleEditorSchema({ - contentType: 'Activity', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.dropdown({ - id: 'title-size', - label: 'Title Size', - options: [ - { label: 'Small', value: 'text-lg' }, - { label: 'Medium', value: 'text-xl' }, - { label: 'Large', value: 'text-2xl' }, - { label: 'Extra Large', value: 'text-3xl' }, - ] - }), - styleEditorField.dropdown({ - id: 'description-size', - label: 'Description Size', - options: [ - { label: 'Small', value: 'text-sm' }, - { label: 'Medium', value: 'text-base' }, - { label: 'Large', value: 'text-lg' }, - ] - }), - styleEditorField.checkboxGroup({ - id: 'title-style', - label: 'Title Style', - options: [ - { label: 'Bold', key: 'bold' }, - { label: 'Italic', key: 'italic' }, - { label: 'Underline', key: 'underline' }, - ] - }), - ] - }, - { - title: 'Layout', - fields: [ - styleEditorField.radio({ - id: 'layout', - label: 'Layout', - columns: 2, - options: [ - { - label: 'Left', - value: 'left', - imageURL: 'https://i.ibb.co/cXv3tfYd/Screenshot-2025-12-23-at-11-58-32-AM.png', - }, - { - label: 'Right', - value: 'right', - imageURL: 'https://i.ibb.co/v4cJxyLZ/Screenshot-2025-12-23-at-11-59-01-AM.png' - }, - { - label: 'Center', - value: 'center', - imageURL: 'https://i.ibb.co/kVntSyzn/Screenshot-2025-12-23-at-11-58-50-AM.png' - }, - { - label: 'Overlap', - value: 'overlap', - imageURL: 'https://i.ibb.co/43Y5KLY/placeholder-icon-design-free-vector.jpg' - }, - ], - }), - styleEditorField.dropdown({ - id: 'image-height', - label: 'Image Height', - options: [ - { label: 'Small', value: 'h-40' }, - { label: 'Medium', value: 'h-56' }, - { label: 'Large', value: 'h-72' }, - { label: 'Extra Large', value: 'h-96' }, - ] - }), - ] - }, - { - title: 'Card Style', - fields: [ - styleEditorField.radio({ - id: 'card-background', - label: 'Card Background', - columns: 2, - options: [ - { - label: 'White', - value: 'white', - }, - { - label: 'Gray', - value: 'gray', - }, - { - label: 'Light Blue', - value: 'light-blue', - }, - { - label: 'Light Green', - value: 'light-green', - }, - ], - }), - styleEditorField.radio({ - id: 'border-radius', - label: 'Border Radius', - columns: 2, - options: [ - { - label: 'None', - value: 'none', - }, - { - label: 'Small', - value: 'small', - }, - { - label: 'Medium', - value: 'medium', - }, - { - label: 'Large', - value: 'large', - }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'card-effects', - label: 'Card Effects', - options: [ - { label: 'Shadow', key: 'shadow' }, - { label: 'Border', key: 'border' }, - ] - }), - ] - }, - { - title: 'Button', - fields: [ - styleEditorField.radio({ - id: 'button-color', - label: 'Button Color', - columns: 2, - options: [ - { - label: 'Blue', - value: 'blue', - }, - { - label: 'Green', - value: 'green', - }, - { - label: 'Red', - value: 'red', - }, - { - label: 'Purple', - value: 'purple', - }, - { - label: 'Orange', - value: 'orange', - }, - { - label: 'Teal', - value: 'teal', - }, - ], - }), - styleEditorField.dropdown({ - id: 'button-size', - label: 'Button Size', - options: [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'button-style', - label: 'Button Style', - options: [ - { label: 'Rounded', key: 'rounded' }, - { label: 'Full Rounded', key: 'full-rounded' }, - { label: 'Shadow', key: 'shadow' }, - ], - }), - ] - }, - ] -}) - -export const BANNER_SCHEMA = defineStyleEditorSchema({ - contentType: 'Banner', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.dropdown({ - id: 'title-size', - label: 'Title Size', - options: [ - { label: 'Small', value: 'text-4xl' }, - { label: 'Medium', value: 'text-5xl' }, - { label: 'Large', value: 'text-6xl' }, - { label: 'Extra Large', value: 'text-7xl' }, - ], - }), - styleEditorField.dropdown({ - id: 'caption-size', - label: 'Caption Size', - options: [ - { label: 'Small', value: 'text-base' }, - { label: 'Medium', value: 'text-lg' }, - { label: 'Large', value: 'text-xl' }, - { label: 'Extra Large', value: 'text-2xl' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'title-style', - label: 'Title Style', - options: [ - { label: 'Bold', key: 'bold' }, - { label: 'Italic', key: 'italic' }, - { label: 'Underline', key: 'underline' }, - ] - }), - ] - }, - { - title: 'Layout', - fields: [ - styleEditorField.radio({ - id: 'text-alignment', - label: 'Text Alignment', - columns: 2, - options: [ - { - label: 'Left', - value: 'left', - }, - { - label: 'Center', - value: 'center', - }, - { - label: 'Right', - value: 'right', - }, - ], - }), - styleEditorField.radio({ - id: 'overlay-style', - label: 'Overlay Style', - columns: 2, - options: [ - { - label: 'None', - value: 'none', - }, - { - label: 'Dark', - value: 'dark', - }, - { - label: 'Light', - value: 'light', - }, - { - label: 'Gradient', - value: 'gradient', - }, - ], - }), - ] - }, - { - title: 'Button', - fields: [ - styleEditorField.radio({ - id: 'button-color', - label: 'Button Color', - columns: 2, - options: [ - { - label: 'Blue', - value: 'blue', - }, - { - label: 'Green', - value: 'green', - }, - { - label: 'Red', - value: 'red', - }, - { - label: 'Purple', - value: 'purple', - }, - { - label: 'Orange', - value: 'orange', - }, - { - label: 'Teal', - value: 'teal', - }, - ], - }), - styleEditorField.dropdown({ - id: 'button-size', - label: 'Button Size', - options: [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'button-style', - label: 'Button Style', - options: [ - { label: 'Rounded', key: 'rounded' }, - { label: 'Full Rounded', key: 'full-rounded' }, - { label: 'Shadow', key: 'shadow' }, - ], - }), - ] - }, - ] -}) \ No newline at end of file diff --git a/examples/astro/src/integration/dotcms/index.ts b/examples/astro/src/integration/dotcms/index.ts index 201d341762ab..c03255c7d293 100644 --- a/examples/astro/src/integration/dotcms/index.ts +++ b/examples/astro/src/integration/dotcms/index.ts @@ -1,3 +1,2 @@ export * from "./getPage"; -export * from "./dotCMSClient"; -export * from "./styleEditorSchemas"; \ No newline at end of file +export * from "./dotCMSClient"; \ No newline at end of file diff --git a/examples/astro/src/integration/dotcms/styleEditorSchemas.ts b/examples/astro/src/integration/dotcms/styleEditorSchemas.ts deleted file mode 100644 index b07f67d8fa51..000000000000 --- a/examples/astro/src/integration/dotcms/styleEditorSchemas.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { defineStyleEditorSchema, styleEditorField } from "@dotcms/uve"; - -export const ACTIVITY_SCHEMA = defineStyleEditorSchema({ - contentType: 'Activity', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.dropdown({ - id: 'title-size', - label: 'Title Size', - options: [ - { label: 'Small', value: 'text-lg' }, - { label: 'Medium', value: 'text-xl' }, - { label: 'Large', value: 'text-2xl' }, - { label: 'Extra Large', value: 'text-3xl' }, - ] - }), - styleEditorField.dropdown({ - id: 'description-size', - label: 'Description Size', - options: [ - { label: 'Small', value: 'text-sm' }, - { label: 'Medium', value: 'text-base' }, - { label: 'Large', value: 'text-lg' }, - ] - }), - styleEditorField.checkboxGroup({ - id: 'title-style', - label: 'Title Style', - options: [ - { label: 'Bold', key: 'bold' }, - { label: 'Italic', key: 'italic' }, - { label: 'Underline', key: 'underline' }, - ] - }), - ] - }, - { - title: 'Layout', - fields: [ - styleEditorField.radio({ - id: 'layout', - label: 'Layout', - columns: 2, - options: [ - { - label: 'Left', - value: 'left', - imageURL: 'https://i.ibb.co/cXv3tfYd/Screenshot-2025-12-23-at-11-58-32-AM.png', - }, - { - label: 'Right', - value: 'right', - imageURL: 'https://i.ibb.co/v4cJxyLZ/Screenshot-2025-12-23-at-11-59-01-AM.png' - }, - { - label: 'Center', - value: 'center', - imageURL: 'https://i.ibb.co/kVntSyzn/Screenshot-2025-12-23-at-11-58-50-AM.png' - }, - { - label: 'Overlap', - value: 'overlap', - imageURL: 'https://i.ibb.co/43Y5KLY/placeholder-icon-design-free-vector.jpg' - }, - ], - }), - styleEditorField.dropdown({ - id: 'image-height', - label: 'Image Height', - options: [ - { label: 'Small', value: 'h-40' }, - { label: 'Medium', value: 'h-56' }, - { label: 'Large', value: 'h-72' }, - { label: 'Extra Large', value: 'h-96' }, - ] - }), - ] - }, - { - title: 'Card Style', - fields: [ - styleEditorField.radio({ - id: 'card-background', - label: 'Card Background', - columns: 2, - options: [ - { - label: 'White', - value: 'white', - }, - { - label: 'Gray', - value: 'gray', - }, - { - label: 'Light Blue', - value: 'light-blue', - }, - { - label: 'Light Green', - value: 'light-green', - }, - ], - }), - styleEditorField.radio({ - id: 'border-radius', - label: 'Border Radius', - columns: 2, - options: [ - { - label: 'None', - value: 'none', - }, - { - label: 'Small', - value: 'small', - }, - { - label: 'Medium', - value: 'medium', - }, - { - label: 'Large', - value: 'large', - }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'card-effects', - label: 'Card Effects', - options: [ - { label: 'Shadow', key: 'shadow' }, - { label: 'Border', key: 'border' }, - ] - }), - ] - }, - { - title: 'Button', - fields: [ - styleEditorField.radio({ - id: 'button-color', - label: 'Button Color', - columns: 2, - options: [ - { - label: 'Blue', - value: 'blue', - }, - { - label: 'Green', - value: 'green', - }, - { - label: 'Red', - value: 'red', - }, - { - label: 'Purple', - value: 'purple', - }, - { - label: 'Orange', - value: 'orange', - }, - { - label: 'Teal', - value: 'teal', - }, - ], - }), - styleEditorField.dropdown({ - id: 'button-size', - label: 'Button Size', - options: [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'button-style', - label: 'Button Style', - options: [ - { label: 'Rounded', key: 'rounded' }, - { label: 'Full Rounded', key: 'full-rounded' }, - { label: 'Shadow', key: 'shadow' }, - ], - }), - ] - }, - ] -}) - -export const BANNER_SCHEMA = defineStyleEditorSchema({ - contentType: 'Banner', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.dropdown({ - id: 'title-size', - label: 'Title Size', - options: [ - { label: 'Small', value: 'text-4xl' }, - { label: 'Medium', value: 'text-5xl' }, - { label: 'Large', value: 'text-6xl' }, - { label: 'Extra Large', value: 'text-7xl' }, - ], - }), - styleEditorField.dropdown({ - id: 'caption-size', - label: 'Caption Size', - options: [ - { label: 'Small', value: 'text-base' }, - { label: 'Medium', value: 'text-lg' }, - { label: 'Large', value: 'text-xl' }, - { label: 'Extra Large', value: 'text-2xl' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'title-style', - label: 'Title Style', - options: [ - { label: 'Bold', key: 'bold' }, - { label: 'Italic', key: 'italic' }, - { label: 'Underline', key: 'underline' }, - ] - }), - ] - }, - { - title: 'Layout', - fields: [ - styleEditorField.radio({ - id: 'text-alignment', - label: 'Text Alignment', - columns: 2, - options: [ - { - label: 'Left', - value: 'left', - }, - { - label: 'Center', - value: 'center', - }, - { - label: 'Right', - value: 'right', - }, - ], - }), - styleEditorField.radio({ - id: 'overlay-style', - label: 'Overlay Style', - columns: 2, - options: [ - { - label: 'None', - value: 'none', - }, - { - label: 'Dark', - value: 'dark', - }, - { - label: 'Light', - value: 'light', - }, - { - label: 'Gradient', - value: 'gradient', - }, - ], - }), - ] - }, - { - title: 'Button', - fields: [ - styleEditorField.radio({ - id: 'button-color', - label: 'Button Color', - columns: 2, - options: [ - { - label: 'Blue', - value: 'blue', - }, - { - label: 'Green', - value: 'green', - }, - { - label: 'Red', - value: 'red', - }, - { - label: 'Purple', - value: 'purple', - }, - { - label: 'Orange', - value: 'orange', - }, - { - label: 'Teal', - value: 'teal', - }, - ], - }), - styleEditorField.dropdown({ - id: 'button-size', - label: 'Button Size', - options: [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'button-style', - label: 'Button Style', - options: [ - { label: 'Rounded', key: 'rounded' }, - { label: 'Full Rounded', key: 'full-rounded' }, - { label: 'Shadow', key: 'shadow' }, - ], - }), - ] - }, - ] -}) diff --git a/examples/astro/src/views/DotCMSPage.tsx b/examples/astro/src/views/DotCMSPage.tsx index b489be4462cf..4a97030abf9a 100644 --- a/examples/astro/src/views/DotCMSPage.tsx +++ b/examples/astro/src/views/DotCMSPage.tsx @@ -1,10 +1,9 @@ -import { DotCMSLayoutBody, useEditableDotCMSPage, useStyleEditorSchemas } from "@dotcms/react"; +import { DotCMSLayoutBody, useEditableDotCMSPage } from "@dotcms/react"; import type { DotCMSCustomPageResponse } from "@/types/page.model"; import { dotComponents } from "@/components/content-types"; import Footer from "@/components/common/Footer"; import Header from "@/components/common/header/Header"; -import { ACTIVITY_SCHEMA, BANNER_SCHEMA } from "@/dotcms-integration"; export const DotCMSPage = ({ @@ -14,8 +13,7 @@ export const DotCMSPage = ({ }) => { const { pageAsset, content } = useEditableDotCMSPage(pageResponse); - - useStyleEditorSchemas([ACTIVITY_SCHEMA, BANNER_SCHEMA]); + const { layout } = pageAsset; const showHeader = layout.header && content; diff --git a/examples/nextjs/src/app/[[...slug]]/page.js b/examples/nextjs/src/app/[[...slug]]/page.js index 0b1fbe799426..8bae2e2d9cb3 100644 --- a/examples/nextjs/src/app/[[...slug]]/page.js +++ b/examples/nextjs/src/app/[[...slug]]/page.js @@ -1,6 +1,6 @@ import NotFound from "@/app/not-found"; -import { Page } from "@/views/Page"; import { getDotCMSPage } from "@/utils/getDotCMSPage"; +import { Page } from "@/views/Page"; export async function generateMetadata(props) { diff --git a/examples/nextjs/src/utils/styleEditorSchemas.js b/examples/nextjs/src/utils/styleEditorSchemas.js deleted file mode 100644 index d5b67006dab6..000000000000 --- a/examples/nextjs/src/utils/styleEditorSchemas.js +++ /dev/null @@ -1,336 +0,0 @@ -import { defineStyleEditorSchema, styleEditorField } from "@dotcms/uve"; - -export const ACTIVITY_SCHEMA = defineStyleEditorSchema({ - contentType: 'Activity', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.dropdown({ - id: 'title-size', - label: 'Title Size', - options: [ - { label: 'Small', value: 'text-lg' }, - { label: 'Medium', value: 'text-xl' }, - { label: 'Large', value: 'text-2xl' }, - { label: 'Extra Large', value: 'text-3xl' }, - ] - }), - styleEditorField.dropdown({ - id: 'description-size', - label: 'Description Size', - options: [ - { label: 'Small', value: 'text-sm' }, - { label: 'Medium', value: 'text-base' }, - { label: 'Large', value: 'text-lg' }, - ] - }), - styleEditorField.checkboxGroup({ - id: 'title-style', - label: 'Title Style', - options: [ - { label: 'Bold', key: 'bold' }, - { label: 'Italic', key: 'italic' }, - { label: 'Underline', key: 'underline' }, - ] - }), - ] - }, - { - title: 'Layout', - fields: [ - styleEditorField.radio({ - id: 'layout', - label: 'Layout', - columns: 2, - options: [ - { - label: 'Left', - value: 'left', - imageURL: 'https://i.ibb.co/cXv3tfYd/Screenshot-2025-12-23-at-11-58-32-AM.png', - }, - { - label: 'Right', - value: 'right', - imageURL: 'https://i.ibb.co/v4cJxyLZ/Screenshot-2025-12-23-at-11-59-01-AM.png' - }, - { - label: 'Center', - value: 'center', - imageURL: 'https://i.ibb.co/kVntSyzn/Screenshot-2025-12-23-at-11-58-50-AM.png' - }, - { - label: 'Overlap', - value: 'overlap', - imageURL: 'https://i.ibb.co/43Y5KLY/placeholder-icon-design-free-vector.jpg' - }, - ], - }), - styleEditorField.dropdown({ - id: 'image-height', - label: 'Image Height', - options: [ - { label: 'Small', value: 'h-40' }, - { label: 'Medium', value: 'h-56' }, - { label: 'Large', value: 'h-72' }, - { label: 'Extra Large', value: 'h-96' }, - ] - }), - ] - }, - { - title: 'Card Style', - fields: [ - styleEditorField.radio({ - id: 'card-background', - label: 'Card Background', - columns: 2, - options: [ - { - label: 'White', - value: 'white', - }, - { - label: 'Gray', - value: 'gray', - }, - { - label: 'Light Blue', - value: 'light-blue', - }, - { - label: 'Light Green', - value: 'light-green', - }, - ], - }), - styleEditorField.radio({ - id: 'border-radius', - label: 'Border Radius', - columns: 2, - options: [ - { - label: 'None', - value: 'none', - }, - { - label: 'Small', - value: 'small', - }, - { - label: 'Medium', - value: 'medium', - }, - { - label: 'Large', - value: 'large', - }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'card-effects', - label: 'Card Effects', - options: [ - { label: 'Shadow', key: 'shadow' }, - { label: 'Border', key: 'border' }, - ] - }), - ] - }, - { - title: 'Button', - fields: [ - styleEditorField.radio({ - id: 'button-color', - label: 'Button Color', - columns: 2, - options: [ - { - label: 'Blue', - value: 'blue', - }, - { - label: 'Green', - value: 'green', - }, - { - label: 'Red', - value: 'red', - }, - { - label: 'Purple', - value: 'purple', - }, - { - label: 'Orange', - value: 'orange', - }, - { - label: 'Teal', - value: 'teal', - }, - ], - }), - styleEditorField.dropdown({ - id: 'button-size', - label: 'Button Size', - options: [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'button-style', - label: 'Button Style', - options: [ - { label: 'Rounded', key: 'rounded' }, - { label: 'Full Rounded', key: 'full-rounded' }, - { label: 'Shadow', key: 'shadow' }, - ], - }), - ] - }, - ] -}) - -export const BANNER_SCHEMA = defineStyleEditorSchema({ - contentType: 'Banner', - sections: [ - { - title: 'Typography', - fields: [ - styleEditorField.dropdown({ - id: 'title-size', - label: 'Title Size', - options: [ - { label: 'Small', value: 'text-4xl' }, - { label: 'Medium', value: 'text-5xl' }, - { label: 'Large', value: 'text-6xl' }, - { label: 'Extra Large', value: 'text-7xl' }, - ], - }), - styleEditorField.dropdown({ - id: 'caption-size', - label: 'Caption Size', - options: [ - { label: 'Small', value: 'text-base' }, - { label: 'Medium', value: 'text-lg' }, - { label: 'Large', value: 'text-xl' }, - { label: 'Extra Large', value: 'text-2xl' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'title-style', - label: 'Title Style', - options: [ - { label: 'Bold', key: 'bold' }, - { label: 'Italic', key: 'italic' }, - { label: 'Underline', key: 'underline' }, - ] - }), - ] - }, - { - title: 'Layout', - fields: [ - styleEditorField.radio({ - id: 'text-alignment', - label: 'Text Alignment', - columns: 2, - options: [ - { - label: 'Left', - value: 'left', - }, - { - label: 'Center', - value: 'center', - }, - { - label: 'Right', - value: 'right', - }, - ], - }), - styleEditorField.radio({ - id: 'overlay-style', - label: 'Overlay Style', - columns: 2, - options: [ - { - label: 'None', - value: 'none', - }, - { - label: 'Dark', - value: 'dark', - }, - { - label: 'Light', - value: 'light', - }, - { - label: 'Gradient', - value: 'gradient', - }, - ], - }), - ] - }, - { - title: 'Button', - fields: [ - styleEditorField.radio({ - id: 'button-color', - label: 'Button Color', - columns: 2, - options: [ - { - label: 'Blue', - value: 'blue', - }, - { - label: 'Green', - value: 'green', - }, - { - label: 'Red', - value: 'red', - }, - { - label: 'Purple', - value: 'purple', - }, - { - label: 'Orange', - value: 'orange', - }, - { - label: 'Teal', - value: 'teal', - }, - ], - }), - styleEditorField.dropdown({ - id: 'button-size', - label: 'Button Size', - options: [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, - ], - }), - styleEditorField.checkboxGroup({ - id: 'button-style', - label: 'Button Style', - options: [ - { label: 'Rounded', key: 'rounded' }, - { label: 'Full Rounded', key: 'full-rounded' }, - { label: 'Shadow', key: 'shadow' }, - ], - }), - ] - }, - ] -}) \ No newline at end of file diff --git a/examples/nextjs/src/views/Page.js b/examples/nextjs/src/views/Page.js index b7323fdda334..6e2a656b69b1 100644 --- a/examples/nextjs/src/views/Page.js +++ b/examples/nextjs/src/views/Page.js @@ -1,18 +1,15 @@ 'use client'; -import { DotCMSLayoutBody, useEditableDotCMSPage, useStyleEditorSchemas } from '@dotcms/react'; +import { DotCMSLayoutBody, useEditableDotCMSPage } from '@dotcms/react'; import { pageComponents } from '@/components/content-types'; import Footer from '@/components/footer/Footer'; import Header from '@/components/header/Header'; -import { ACTIVITY_SCHEMA, BANNER_SCHEMA } from '@/utils/styleEditorSchemas'; export function Page({ pageContent }) { const { pageAsset, content = {} } = useEditableDotCMSPage(pageContent); const navigation = content.navigation; - useStyleEditorSchemas([ACTIVITY_SCHEMA, BANNER_SCHEMA]); - return (
{pageAsset?.layout.header &&
}