diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index edfde776e2..15a4b9670e 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -526,8 +526,12 @@ internal abstract class ContentEditingServiceBaseError! This property type is no longer available. Please reach out to your administrator.

', + "Don't worry, your content is safe and publishing this document won't overwrite it or remove it.
Please contact your site administrator to resolve this issue.", + detailsTitle: 'Additional details', detailsDescription: - '

This property type is no longer available.
Please contact your administrator so they can either delete this property or restore the property type.

Data:

', + "To resolve this you should either restore the property editor, change the property to use a supported data type or remove the property if it's no longer needed.", + detailsDataType: 'Data type', + detailsPropertyEditor: 'Property editor', + detailsData: 'Data', + detailsHide: 'Hide details', + detailsShow: 'Show details', }, uiCulture: { ar: 'العربية', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts index cd63947ae4..36ce12d20d 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts @@ -2832,9 +2832,16 @@ export default { resetUrlLabel: 'Redefinir', }, missingEditor: { + title: 'Este tipo de propriedade já não se encontra disponível.', description: - '

Erro! Este tipo de propriedade já não se encontra disponível. Por favor, contacte o administrador.

', + 'Não se preocupe, o seu conteúdo está seguro e a publicação deste documento não o substituirá nem removerá.
Entre em contacto com o administrador do site para resolver o problema.', + detailsTitle: 'Detalhes adicionais', detailsDescription: - '

Este tipo de propriedade já não se encontra disponível.
Por favor, contacte o administrador para que ele possa apagar a propriedade ou restaurar o tipo de propriedade.

Dados:

', + 'Para resolver o problema, deverá ou restaurar o editor de propriedades, ou alterar a propriedade para usar um tipo de dados compatível ou remover a propriedade se ela não for mais necessária.', + detailsDataType: 'Tipo de dados', + detailsPropertyEditor: 'Editor de propriedades', + detailsData: 'Dados', + detailsHide: 'Esconder detalhes', + detailsShow: 'Mostrar detalhes', }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts index 0575dfc63a..932e48b20f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts @@ -1,5 +1,3 @@ -import { manifests as modalManifests } from './modal/manifests.js'; - export const manifests: Array = [ { type: 'propertyEditorUi', @@ -14,5 +12,4 @@ export const manifests: Array = [ supportsReadOnly: true, }, }, - ...modalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts deleted file mode 100644 index fb0853adfa..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './missing-editor-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts deleted file mode 100644 index 3ef10f367f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const manifests: Array = [ - { - type: 'modal', - alias: 'Umb.Modal.MissingPropertyEditor', - name: 'Missing Property Editor Modal', - element: () => import('./missing-editor-modal.element.js'), - }, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts deleted file mode 100644 index f71d9769aa..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { UmbMissingPropertyModalData, UmbMissingPropertyModalResult } from './missing-editor-modal.token.js'; -import { html, customElement, css } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; - -@customElement('umb-missing-property-editor-modal') -export class UmbMissingPropertyEditorModalElement extends UmbModalBaseElement< - UmbMissingPropertyModalData, - UmbMissingPropertyModalResult -> { - override render() { - return html` - - - ${this.data?.value} - - - `; - } - - static override styles = [ - UmbTextStyles, - css` - uui-dialog-layout { - max-inline-size: 60ch; - } - #codeblock { - max-height: 300px; - overflow: auto; - } - `, - ]; -} - -export { UmbMissingPropertyEditorModalElement as element }; - -declare global { - interface HTMLElementTagNameMap { - 'umb-missing-property-editor-modal': UmbMissingPropertyEditorModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts deleted file mode 100644 index 9792759058..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; - -export interface UmbMissingPropertyModalData { - value: string | undefined; -} - -export type UmbMissingPropertyModalResult = undefined; - -export const UMB_MISSING_PROPERTY_EDITOR_MODAL = new UmbModalToken< - UmbMissingPropertyModalData, - UmbMissingPropertyModalResult ->('Umb.Modal.MissingPropertyEditor', { - modal: { - type: 'dialog', - size: 'small', - }, -}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts index 5ec66cbf83..337dd9fe9e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts @@ -1,50 +1,129 @@ -import { UMB_MISSING_PROPERTY_EDITOR_MODAL } from './modal/missing-editor-modal.token.js'; -import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, query, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UMB_PROPERTY_TYPE_BASED_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UmbDataTypeDetailRepository, type UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; /** * @element umb-property-editor-ui-missing */ @customElement('umb-property-editor-ui-missing') -export class UmbPropertyEditorUIMissingElement - extends UmbFormControlMixin(UmbLitElement, undefined) - implements UmbPropertyEditorUiElement -{ +export class UmbPropertyEditorUIMissingElement extends UmbLitElement implements UmbPropertyEditorUiElement { + @property() + value = ''; + + @state() + private _expanded = false; + + @query('#details') + focalPointElement!: HTMLElement; + + private _dataTypeDetailModel?: UmbDataTypeDetailModel | undefined; + private _dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); + constructor() { super(); - this.addValidator( - 'customError', - () => this.localize.term('errors_propertyHasErrors'), - () => true, - ); - - this.pristine = false; + this.consumeContext(UMB_PROPERTY_TYPE_BASED_PROPERTY_CONTEXT, (propertyContext) => { + if (!propertyContext?.dataType) return; + this.observe(propertyContext.dataType, (dt) => { + if (!dt?.unique) return; + this._updateEditorAlias(dt); + }); + }); } - async #onDetails(event: Event) { - event.stopPropagation(); + private async _updateEditorAlias(dataType: UmbPropertyTypeModel['dataType']) { + this.observe(await this._dataTypeDetailRepository.byUnique(dataType.unique), (dataType) => { + this._dataTypeDetailModel = dataType; + }); + } - await umbOpenModal(this, UMB_MISSING_PROPERTY_EDITOR_MODAL, { - data: { - // If the value is an object, we stringify it to make sure we can display it properly. - // If it's a primitive value, we just convert it to string. - value: typeof this.value === 'object' ? JSON.stringify(this.value, null, 2) : String(this.value), - }, - }).catch(() => undefined); + async #onDetails() { + this._expanded = !this._expanded; + if (this._expanded) { + await this.updateComplete; + this.focalPointElement?.focus(); + } } override render() { - return html` + return html` +
+ ${this.localize.term('missingEditor_title')} +
+
+ + ${this._expanded ? this._renderDetails() : nothing} +
+ `; + compact + label="${this.localize.term(this._expanded ? 'missingEditor_detailsHide' : 'missingEditor_detailsShow')}" + @click=${this.#onDetails}> + ${this.localize.term(this._expanded ? 'missingEditor_detailsHide' : 'missingEditor_detailsShow')} + +
`; } + + private _renderDetails() { + return html`
+ +

+ +

+

+ : + ${this._dataTypeDetailModel?.name}
+ : + ${this._dataTypeDetailModel?.editorAlias} +

+ ${typeof this.value === 'object' ? JSON.stringify(this.value, null, 2) : String(this.value)} +
`; + } + + static override styles = [ + css` + :host { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-3); + --uui-box-default-padding: 0; + } + #content { + padding: var(--uui-size-space-5); + padding-bottom: var(--uui-size-space-3); + } + #alert { + padding-right: var(--uui-size-space-2); + } + #details-button { + float: right; + } + #details { + margin-top: var(--uui-size-space-5); + } + #details-title { + font-weight: 800; + } + #expand-symbol { + transform: rotate(90deg); + } + #expand-symbol[open] { + transform: rotate(180deg); + } + #codeblock { + max-height: 400px; + display: flex; + flex-direction: column; + } + `, + ]; } export default UmbPropertyEditorUIMissingElement; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs index 3457ba7719..3d5eacb428 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Serialization; @@ -27,16 +28,14 @@ public class PropertyValidationServiceTests private void MockObjects(out PropertyValidationService validationService, out IDataType dt) { var dataTypeService = new Mock(); - var dataType = Mock.Of( - x => x.ConfigurationObject == string.Empty // irrelevant but needs a value - && x.DatabaseType == ValueStorageType.Nvarchar - && x.EditorAlias == Constants.PropertyEditors.Aliases.TextBox); + var dataType = Mock.Of(x => x.ConfigurationObject == string.Empty // irrelevant but needs a value + && x.DatabaseType == ValueStorageType.Nvarchar + && x.EditorAlias == Constants.PropertyEditors.Aliases.TextBox); dataTypeService.Setup(x => x.GetDataType(It.IsAny())).Returns(() => dataType); dt = dataType; // new data editor that returns a TextOnlyValueEditor which will do the validation for the properties - var dataEditor = Mock.Of( - x => x.Alias == Constants.PropertyEditors.Aliases.TextBox); + var dataEditor = Mock.Of(x => x.Alias == Constants.PropertyEditors.Aliases.TextBox); Mock.Get(dataEditor).Setup(x => x.GetValueEditor(It.IsAny())) .Returns(new CustomTextOnlyValueEditor( new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox), @@ -44,7 +43,15 @@ public class PropertyValidationServiceTests new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()), Mock.Of())); - var propEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); + var languageService = new Mock(); + languageService + .Setup(s => s.GetDefaultIsoCodeAsync()) + .ReturnsAsync(() => "en-US"); + + var propEditors = new PropertyEditorCollection(new DataEditorCollection(() => [dataEditor])); + + var contentSettings = new Mock>(); + contentSettings.Setup(x => x.Value).Returns(new ContentSettings()); validationService = new PropertyValidationService( propEditors, @@ -52,8 +59,8 @@ public class PropertyValidationServiceTests Mock.Of(), new ValueEditorCache(), Mock.Of(), - Mock.Of(), - Mock.Of>()); + languageService.Object, + contentSettings.Object); } [Test] @@ -279,6 +286,23 @@ public class PropertyValidationServiceTests Assert.AreEqual(4, invalid.Length); } + [TestCase(null)] + [TestCase(24)] + [TestCase("test")] + [TestCase("{\"test\": true}")] + public void ValidatePropertyValue_Always_Returns_No_Validation_Errors_For_Missing_Editor(object? value) + { + MockObjects(out var validationService, out _); + + var p1 = new PropertyType(ShortStringHelper, "Missing.Alias", ValueStorageType.Ntext) + { + Variations = ContentVariation.Nothing, + }; + + var result = validationService.ValidatePropertyValue(p1, value, PropertyValidationContext.Empty()); + Assert.AreEqual(0, result.Count()); + } + // used so we can inject a mock - we should fix the base class DataValueEditor to be able to have the ILocalizedTextField passed // in to create the Requried and Regex validators so we aren't using singletons private class CustomTextOnlyValueEditor : TextOnlyValueEditor