diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs index e4b60877f1..a4cabf51bb 100644 --- a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs @@ -42,7 +42,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions { if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor - && controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name)) + && controllerActionDescriptor.HasMapToApiAttribute(name)) { return true; } diff --git a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs new file mode 100644 index 0000000000..070800d35f --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc.Abstractions; +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Common.Configuration; + +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for to work with . +/// +public static class ActionDescriptorApiCommonExtensions +{ + /// + /// Determines whether the has a with the specified API name. + /// The check is made in runtime to support attributes added in runtime. + /// + /// The action descriptor to inspect. + /// The API name to check for. + /// + /// true if the is present and matches the specified API name, + /// or if the attribute is not present and the API name matches the default API name; otherwise, false. + /// + public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, string apiName) + { + var value = actionDescriptor.GetMapToApiAttributeValue(); + + return value == apiName + || (value is null && apiName == DefaultApiConfiguration.ApiName); + } + + private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) + { + IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; + + return mapToApiAttributes.SingleOrDefault()?.ApiName; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/AppCacheExtensions.cs b/src/Umbraco.Core/Cache/AppCacheExtensions.cs index 480b677f24..32c1b772f0 100644 --- a/src/Umbraco.Core/Cache/AppCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/AppCacheExtensions.cs @@ -41,27 +41,39 @@ public static class AppCacheExtensions public static T? GetCacheItem(this IAppCache provider, string cacheKey) { var result = provider.Get(cacheKey); - if (IsRetrievedItemNull(result)) + if (result == null) { return default; } + // If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string). + // Otherwise consider it a null value. + if (RetrievedNullRepresentationInCache(result)) + { + return RequestedNullRepresentationInCache() ? (T)result : default; + } + return result.TryConvertTo().Result; } public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) { var result = provider.Get(cacheKey, () => getCacheItem()); - if (IsRetrievedItemNull(result)) + if (result == null) { return default; } + // If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string). + // Otherwise consider it a null value. + if (RetrievedNullRepresentationInCache(result)) + { + return RequestedNullRepresentationInCache() ? (T)result : default; + } + return result.TryConvertTo().Result; } - private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache; - public static async Task GetCacheItemAsync( this IAppPolicyCache provider, string cacheKey, @@ -77,9 +89,25 @@ public static class AppCacheExtensions provider.Insert(cacheKey, () => result, timeout, isSliding); } - return result == null ? default : result.TryConvertTo().Result; + if (result == null) + { + return default; + } + + // If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string). + // Otherwise consider it a null value. + if (RetrievedNullRepresentationInCache(result)) + { + return RequestedNullRepresentationInCache() ? (T)result : default; + } + + return result.TryConvertTo().Result; } + private static bool RetrievedNullRepresentationInCache(object result) => result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache; + + private static bool RequestedNullRepresentationInCache() => typeof(T) == typeof(string); + public static async Task InsertCacheItemAsync( this IAppPolicyCache provider, string cacheKey, diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts index be04a04f78..8897d4fb82 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -2546,6 +2546,8 @@ export default { pickSpecificAllowance: 'Tilføj gruppe eller blok', allowanceMinimum: 'Sæt minimum krav', allowanceMaximum: 'Sæt maksimum krav', + rangeAllowed: 'Antal blokke', + specifiedAllowance: 'Tilladte bloktyper', block: 'Blok', tabBlock: 'Blok', tabBlockTypeSettings: 'Indstillinger', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 09c96011ce..f314f153d1 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2681,6 +2681,8 @@ export default { pickSpecificAllowance: 'Pick group or Block', allowanceMinimum: 'Set a minimum requirement', allowanceMaximum: 'Set a maximum requirement', + rangeAllowed: 'Number of blocks', + specifiedAllowance: 'Allowed block types', block: 'Block', tabBlock: 'Block', tabBlockTypeSettings: 'Settings', @@ -2847,9 +2849,17 @@ export default { "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', + detailsPropertyEditorUi: 'Property editor UI', detailsData: 'Data', detailsHide: 'Hide details', detailsShow: 'Show details', + missingUiTitle: 'The configured property editor UI could not be found.', + missingUiDetailsDescription: + 'This property editor UI is missing. Ensure your custom UI is registered correctly and the alias matches your configuration.
For implementation details, refer to the documentation.', + dataTypeMissingEditor: 'Property Editor not found', + dataTypeMissingEditorMessage: 'This property editor could not be found.', + dataTypeMissingEditorUi: 'Property Editor UI not found', + dataTypeMissingEditorUiMessage: 'This property editor UI could not be found.', }, dateTimePicker: { local: 'Local', 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 f0777b51cd..9381e7e6c1 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts @@ -2842,9 +2842,17 @@ export default { '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', + detailsPropertyEditorUi: 'Interface do editor de propriedades', detailsData: 'Dados', detailsHide: 'Esconder detalhes', detailsShow: 'Mostrar detalhes', + missingUiTitle: 'A interface do editor de propriedades configurada não foi encontrada.', + missingUiDetailsDescription: + 'Esta interface do editor de propriedades não foi encontrada. Certifique-se de que esta está registada corretamente e que o alias corresponde à sua configuração.
Para detalhes de implementação, consulte a documentação.', + dataTypeMissingEditor: 'Editor de propriedades não encontrado', + dataTypeMissingEditorMessage: 'Este editor de propriedades não foi encontrado.', + dataTypeMissingEditorUi: 'Interface do editor de propriedades não encontrada', + dataTypeMissingEditorUiMessage: 'Esta interface do editor de propriedades não foi encontrada.', }, dateTimePicker: { local: 'Local', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/settings.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/settings.element.ts index 773ba309c6..830dcdd406 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/views/settings.element.ts @@ -58,7 +58,7 @@ export class UmbBlockGridAreaTypeWorkspaceViewSettingsElement extends UmbLitElem property-editor-ui-alias="Umb.PropertyEditorUi.TextBox"> - + diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts index c829c22721..98799e291f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts @@ -33,7 +33,7 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement private _blockTypes?: Array; @state() - private _blockTypesWithElementName: Array<{ type: UmbBlockTypeWithGroupKey; name: string }> = []; + private _blockTypesWithElementName: Array<{ type: UmbBlockTypeWithGroupKey; name: string; icon: string | null | undefined }> = []; @state() private _blockGroups: Array = []; @@ -51,11 +51,11 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement .map((item) => { const blockType = this._blockTypes?.find((block) => block.contentElementTypeKey === item.unique); if (blockType) { - return { type: blockType, name: item.name }; + return { type: blockType, name: item.name, icon: item.icon }; } return undefined; }) - .filter((x) => x !== undefined) as Array<{ type: UmbBlockTypeWithGroupKey; name: string }>; + .filter((x) => x !== undefined) as Array<{ type: UmbBlockTypeWithGroupKey; name: string; icon: string | null | undefined }>; }); this.consumeContext(UMB_DATA_TYPE_WORKSPACE_CONTEXT, async (context) => { @@ -193,7 +193,10 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement this._blockGroups, (group) => group.key, (group) => - html` + html` + ${group.name} `, ); @@ -207,6 +210,7 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement html` + ${block.name} `, ); @@ -245,6 +249,13 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement uui-combobox strong { padding: 0 var(--uui-size-space-1); } + + uui-combobox-list-option { + display: flex; + align-items: center; + gap: var(--uui-size-space-2); + padding: var(--uui-size-2); + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts index 6fed4cef67..36f6ec4345 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts @@ -22,12 +22,12 @@ import type { UUIColorPickerElement, UUIInputElement, UUIInputEvent } from '@umb export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin(UmbLitElement, '') { @property({ type: String }) public override set value(value: string) { + this._valueHex = this.#expandHex(value); + if (value.startsWith('#')) { - this._valueHex = value; super.value = value.substring(1); } else { super.value = value; - this._valueHex = `#${value}`; } } public override get value() { @@ -67,6 +67,17 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin( @property({ type: Boolean }) showLabels = false; + #expandHex(hex: string) { + hex = hex.replace(/^#/, ''); + + // If it's 3-digit, expand it + if (hex.length === 3) { + hex = hex.split('').map(ch => ch + ch).join(''); + } + + return `#${hex}`; + } + async #onDelete() { await umbConfirmModal(this, { headline: `${this.localize.term('actions_delete')} ${this.value || ''}`, @@ -149,7 +160,7 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin( return html`
- ${this.disabled || this.readonly ? nothing : html``} + ${this.disabled || this.readonly ? nothing : html``}
- +
${when( this.showLabels, @@ -249,6 +265,14 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin( margin: 0; position: absolute; } + + .handle { + cursor: grab; + } + + .handle:active { + cursor: grabbing; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/constants.ts index 169e03657c..8bf80e8366 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/constants.ts @@ -1 +1,3 @@ export const UMB_PROPERTY_EDITOR_SCHEMA_ALIAS_DEFAULT = 'Umbraco.Label'; +export const UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS = 'Umb.PropertyEditorUi.Missing'; +export const UMB_MISSING_PROPERTY_EDITOR_UI_UI_ALIAS = 'Umb.PropertyEditorUi.MissingUi'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts index c9067d0cc7..c46ecc5059 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts @@ -9,10 +9,11 @@ import { UmbFormControlValidator, UmbObserveValidationStateController, } from '@umbraco-cms/backoffice/validation'; -import type { - ManifestPropertyEditorUi, - UmbPropertyEditorConfigCollection, - UmbPropertyEditorConfig, +import { + type ManifestPropertyEditorUi, + type UmbPropertyEditorConfigCollection, + type UmbPropertyEditorConfig, + UMB_MISSING_PROPERTY_EDITOR_UI_UI_ALIAS, } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyTypeAppearanceModel, @@ -294,6 +295,11 @@ export class UmbPropertyElement extends UmbLitElement { this.observe( umbExtensionsRegistry.byTypeAndAlias('propertyEditorUi', this._propertyEditorUiAlias), (manifest) => { + if (!manifest && this._propertyEditorUiAlias !== UMB_MISSING_PROPERTY_EDITOR_UI_UI_ALIAS) { + this._propertyEditorUiAlias = UMB_MISSING_PROPERTY_EDITOR_UI_UI_ALIAS; + this._observePropertyEditorUI(); + return; + } this._gotEditorUI(manifest); }, '_observePropertyEditorUI', @@ -307,7 +313,6 @@ export class UmbPropertyElement extends UmbLitElement { this.#propertyContext.setEditorManifest(manifest ?? undefined); if (!manifest) { - // TODO: if propertyEditorUiAlias didn't exist in store, we should do some nice fail UI. return; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts index b5e71345d4..6c12975cd0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts @@ -368,7 +368,9 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal if (hasMessages === false && resultsStatus === false) { const notValidValidators = this.#validators.filter((v) => v.isValid === false); console.warn( - 'Missing validation messages to represent why a child validation context is invalid. These Validators was not valid, one of these did not set a message to represent their state:', + `Missing validation messages to represent why a child validation context is invalid. + This could be because the Validator does not have a 'data-path' and therefore not able to set a message to the Validation Context. + These Validators was not valid, one of these did not set a message to represent their state:`, notValidValidators, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts index 8f309f0858..e273a92aa6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts @@ -12,6 +12,7 @@ import type { UUIInputElement, UUIPopoverContainerElement } from '@umbraco-cms/b import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UMB_HINT_CONTEXT } from '@umbraco-cms/backoffice/hint'; import type { UmbHint, UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; @customElement('umb-workspace-split-view-variant-selector') export class UmbWorkspaceSplitViewVariantSelectorElement< @@ -137,6 +138,24 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< }, '_observeVariantOptions', ); + + if (workspaceContext) { + this.observe( + observeMultiple([ + workspaceContext.variesByCulture, + workspaceContext.variesBySegment, + workspaceContext.variantOptions, + ]), + ([variesByCulture, variesBySegment, variantOptions]) => { + if (variesByCulture === false && variesBySegment === true && variantOptions.length > 1) { + this.#expandVariant(UmbVariantId.Create(variantOptions[0])); + } + }, + '_observeExpandFirstVariantIfSegmentOnly', + ); + } else { + this.removeUmbControllerByAlias('_observeExpandFirstVariantIfSegmentOnly'); + } } async #observeActiveVariants(workspaceContext?: UmbVariantDatasetWorkspaceContext) { @@ -266,12 +285,12 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< // If the active variant is a segment then we expend the culture variant when the selector is opened. if (this.#isSegmentVariantOption(this._activeVariant)) { - const culture = this._cultureVariantOptions.find((variant) => { + const option = this._cultureVariantOptions.find((variant) => { return variant.culture === this._activeVariant?.culture && variant.segment === null; }); - if (!culture) return; - const variantId = UmbVariantId.Create(culture); + if (!option) return; + const variantId = UmbVariantId.Create(option); this.#expandVariant(variantId); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts index 6d4983f6b6..eab6925556 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts @@ -1,4 +1,5 @@ import './components/index.js'; +import './workspace/index.js'; export * from './components/index.js'; export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts index 993986e58e..e9176832de 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts @@ -195,7 +195,10 @@ export class UmbDataTypeWorkspaceContext weight: x.weight ?? 1000 + i, })); this.#propertyEditorUISettingsDefaultData = manifest?.meta.settings?.defaultData || []; - this.setPropertyEditorSchemaAlias(manifest?.meta.propertyEditorSchemaAlias); + const manifestPropertyEditorSchemaAlias = manifest?.meta.propertyEditorSchemaAlias; + if (manifestPropertyEditorSchemaAlias) { + this.setPropertyEditorSchemaAlias(manifestPropertyEditorSchemaAlias); + } this.#mergeConfigProperties(); }, 'editorUi', diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/index.ts new file mode 100644 index 0000000000..22b01be7b7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/index.ts @@ -0,0 +1 @@ +import './views/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-property-editor-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-property-editor-picker.element.ts new file mode 100644 index 0000000000..621aab6f8e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-property-editor-picker.element.ts @@ -0,0 +1,158 @@ +import { UMB_DATA_TYPE_WORKSPACE_CONTEXT } from '../../data-type-workspace.context-token.js'; +import { css, customElement, html, nothing, property, ref } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIButtonElement } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import { + UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS, + UMB_PROPERTY_EDITOR_UI_PICKER_MODAL, +} from '@umbraco-cms/backoffice/property-editor'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + +/** + * @internal should only be used in the data type workspace. + */ +@customElement('umb-data-type-details-workspace-property-editor-picker') +export class UmbDataTypeDetailsWorkspacePropertyEditorPickerElement extends UmbFormControlMixin< + undefined, + typeof UmbLitElement, + undefined +>(UmbLitElement) { + @property({ type: String }) + propertyEditorUiIcon?: string; + + @property({ type: String }) + propertyEditorUiName?: string; + + @property({ type: String }) + propertyEditorUiAlias?: string; + + @property({ type: String }) + propertyEditorSchemaAlias?: string; + + #workspaceContext?: typeof UMB_DATA_TYPE_WORKSPACE_CONTEXT.TYPE; + #addButton?: UUIButtonElement; + + constructor() { + super(); + + this.consumeContext(UMB_DATA_TYPE_WORKSPACE_CONTEXT, (workspaceContext) => { + this.#workspaceContext = workspaceContext; + }); + + this.addValidator( + 'customError', + () => this.localize.term('missingEditor_dataTypeMissingEditorUiMessage'), + () => !this.propertyEditorUiName, + ); + + this.addValidator( + 'customError', + () => this.localize.term('missingEditor_dataTypeMissingEditorMessage'), + () => this.propertyEditorUiAlias === UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS, + ); + } + + protected override getFormElement() { + return undefined; + } + + #addButtonRefChanged(input?: Element) { + if (this.#addButton) { + this.removeFormControlElement(this.#addButton); + } + this.#addButton = input as UUIButtonElement | undefined; + if (this.#addButton) { + this.addFormControlElement(this.#addButton); + } + } + + async #openPropertyEditorUIPicker() { + const value = await umbOpenModal(this, UMB_PROPERTY_EDITOR_UI_PICKER_MODAL, { + value: { + selection: this.propertyEditorUiAlias ? [this.propertyEditorUiAlias] : [], + }, + }).catch(() => undefined); + + if (value) { + this.#workspaceContext?.setPropertyEditorUiAlias(value.selection[0]); + } + } + + #renderPropertyEditorReference() { + if (!this.propertyEditorUiAlias || !this.propertyEditorSchemaAlias) return nothing; + + let name = this.propertyEditorUiName; + let alias = this.propertyEditorUiAlias; + let error = false; + + if (!this.propertyEditorUiName) { + name = this.localize.term('missingEditor_dataTypeMissingEditorUi'); + error = true; + } + + if (this.propertyEditorUiAlias === UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS) { + name = this.localize.term('missingEditor_dataTypeMissingEditor'); + alias = ''; + error = true; + } + + return html` + + ${this.propertyEditorUiIcon + ? html`` + : nothing} + + + + + `; + } + + #renderChooseButton() { + return html` + + `; + } + + override render() { + return html` + ${this.propertyEditorUiAlias && this.propertyEditorSchemaAlias + ? this.#renderPropertyEditorReference() + : this.#renderChooseButton()} + `; + } + + static override styles = [ + UmbTextStyles, + css` + #btn-add { + display: block; + } + `, + ]; +} + +export default UmbDataTypeDetailsWorkspacePropertyEditorPickerElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-data-type-details-workspace-property-editor-picker': UmbDataTypeDetailsWorkspacePropertyEditorPickerElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts index 073774fe8d..c49b12a2ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts @@ -1,25 +1,24 @@ import { UMB_DATA_TYPE_WORKSPACE_CONTEXT } from '../../data-type-workspace.context-token.js'; import { css, customElement, html, nothing, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS } from '@umbraco-cms/backoffice/property-editor'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; -import { UMB_PROPERTY_EDITOR_UI_PICKER_MODAL } from '@umbraco-cms/backoffice/property-editor'; -import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; import { umbBindToValidation } from '@umbraco-cms/backoffice/validation'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; @customElement('umb-data-type-details-workspace-view') export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { @state() - private _propertyEditorUiIcon?: string | null = null; + private _propertyEditorUiIcon?: string; @state() - private _propertyEditorUiName?: string | null = null; + private _propertyEditorUiName?: string; @state() - private _propertyEditorUiAlias?: string | null = null; + private _propertyEditorUiAlias?: string; @state() - private _propertyEditorSchemaAlias?: string | null = null; + private _propertyEditorSchemaAlias?: string; #workspaceContext?: typeof UMB_DATA_TYPE_WORKSPACE_CONTEXT.TYPE; @@ -38,47 +37,52 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im } this.observe(this.#workspaceContext.propertyEditorUiAlias, (value) => { - this._propertyEditorUiAlias = value; + this._propertyEditorUiAlias = value ?? undefined; }); this.observe(this.#workspaceContext.propertyEditorSchemaAlias, (value) => { - this._propertyEditorSchemaAlias = value; + this._propertyEditorSchemaAlias = value ?? undefined; }); this.observe(this.#workspaceContext.propertyEditorUiName, (value) => { - this._propertyEditorUiName = value; + this._propertyEditorUiName = value ?? undefined; }); this.observe(this.#workspaceContext.propertyEditorUiIcon, (value) => { - this._propertyEditorUiIcon = value; + this._propertyEditorUiIcon = value ?? undefined; }); } - async #openPropertyEditorUIPicker() { - const value = await umbOpenModal(this, UMB_PROPERTY_EDITOR_UI_PICKER_MODAL, { - value: { - selection: this._propertyEditorUiAlias ? [this._propertyEditorUiAlias] : [], - }, - }).catch(() => undefined); - - if (value) { - this.#workspaceContext?.setPropertyEditorUiAlias(value.selection[0]); - } - } - override render() { return html` - ${this._propertyEditorUiAlias && this._propertyEditorSchemaAlias - ? this.#renderPropertyEditorReference() - : this.#renderChooseButton()} + + + + ${this.#renderSettings()} `; } #renderSettings() { - if (!this._propertyEditorUiAlias || !this._propertyEditorSchemaAlias) return nothing; + if ( + !this._propertyEditorUiAlias || + !this._propertyEditorUiName || + !this._propertyEditorSchemaAlias || + this._propertyEditorUiAlias === UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS + ) + return nothing; return html` @@ -86,55 +90,6 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im `; } - // Notice, we have implemented a property-layout for each states of the property editor ui picker, in this way the validation message gets removed once the choose-button is gone. (As we are missing ability to detect if elements got removed from DOM)[NL] - #renderChooseButton() { - return html` - - - - `; - } - - #renderPropertyEditorReference() { - if (!this._propertyEditorUiAlias || !this._propertyEditorSchemaAlias) return nothing; - return html` - - - ${this._propertyEditorUiIcon - ? html`` - : nothing} - - - - - - `; - } - static override styles = [ UmbTextStyles, css` diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/index.ts new file mode 100644 index 0000000000..183d3e061a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/index.ts @@ -0,0 +1 @@ +import './details/data-type-details-workspace-property-editor-picker.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts index ead2224586..45ef5f33bc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts @@ -5,8 +5,7 @@ import { customElement, html, state } from '@umbraco-cms/backoffice/external/lit import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbWorkspaceSplitViewVariantSelectorElement } from '@umbraco-cms/backoffice/workspace'; -const elementName = 'umb-document-workspace-split-view-variant-selector'; -@customElement(elementName) +@customElement('umb-document-workspace-split-view-variant-selector') export class UmbDocumentWorkspaceSplitViewVariantSelectorElement extends UmbWorkspaceSplitViewVariantSelectorElement { protected override _variantSorter = sortVariants; @@ -68,6 +67,6 @@ export class UmbDocumentWorkspaceSplitViewVariantSelectorElement extends UmbWork declare global { interface HTMLElementTagNameMap { - [elementName]: UmbDocumentWorkspaceSplitViewVariantSelectorElement; + 'umb-document-workspace-split-view-variant-selector': UmbDocumentWorkspaceSplitViewVariantSelectorElement; } } 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 932e48b20f..f4612f6754 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 @@ -5,9 +5,22 @@ export const manifests: Array = [ name: 'Missing Property Editor UI', element: () => import('./property-editor-ui-missing.element.js'), meta: { - label: 'Missing', + label: 'Missing Property Editor', propertyEditorSchemaAlias: undefined, // By setting it to undefined, this editor won't appear in the property editor UI picker modal. - icon: 'icon-ordered-list', + icon: 'icon-circle-dotted', + group: '', + supportsReadOnly: true, + }, + }, + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.MissingUi', + name: 'Missing Property Editor UI UI', + element: () => import('./property-editor-ui-missing-ui.element.js'), + meta: { + label: 'Missing Property Editor UI', + propertyEditorSchemaAlias: undefined, // By setting it to undefined, this editor won't appear in the property editor UI picker modal. + icon: 'icon-circle-dotted', group: '', supportsReadOnly: true, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing-base.element.ts new file mode 100644 index 0000000000..0db317ce23 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing-base.element.ts @@ -0,0 +1,132 @@ +import { css, html, nothing, property, query, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +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'; + +export class UmbPropertyEditorUIMissingBaseElement extends UmbLitElement implements UmbPropertyEditorUiElement { + @property() + value = ''; + + @state() + private _expanded = false; + + @query('#details') + focalPointElement!: HTMLElement; + + private _dataTypeDetailModel?: UmbDataTypeDetailModel | undefined; + private _dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); + + protected _titleKey: string = ''; + protected _detailsDescriptionKey: string = ''; + protected _displayPropertyEditorUi: boolean = true; + + constructor() { + super(); + + 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); + }); + }); + } + + private async _updateEditorAlias(dataType: UmbPropertyTypeModel['dataType']) { + this.observe(await this._dataTypeDetailRepository.byUnique(dataType.unique), (dataType) => { + this._dataTypeDetailModel = dataType; + }); + } + + async #onDetails() { + this._expanded = !this._expanded; + if (this._expanded) { + await this.updateComplete; + this.focalPointElement?.focus(); + } + } + + override render() { + return html` +
${this.localize.term(this._titleKey)}
+
+ + ${this._expanded ? this._renderDetails() : nothing} +
+ + + ${this.localize.term(this._expanded ? 'missingEditor_detailsHide' : 'missingEditor_detailsShow')} + +
`; + } + + private _renderDetails() { + return html`
+ +

+ +

+

+ : + ${this._dataTypeDetailModel?.name}
+ : + ${this._dataTypeDetailModel?.editorAlias} + ${this._displayPropertyEditorUi + ? html` +
+ : + ${this._dataTypeDetailModel?.editorUiAlias} + ` + : nothing} +

+ ${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; + } + `, + ]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing-ui.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing-ui.element.ts new file mode 100644 index 0000000000..c132584a84 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing-ui.element.ts @@ -0,0 +1,23 @@ +import { UmbPropertyEditorUIMissingBaseElement } from './property-editor-ui-missing-base.element.js'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; + +/** + * @element umb-property-editor-ui-missing-ui + */ +@customElement('umb-property-editor-ui-missing-ui') +export class UmbPropertyEditorUIMissingUiElement extends UmbPropertyEditorUIMissingBaseElement { + constructor() { + super(); + this._titleKey = 'missingEditor_missingUiTitle'; + this._detailsDescriptionKey = 'missingEditor_missingUiDetailsDescription'; + this._displayPropertyEditorUi = true; + } +} + +export default UmbPropertyEditorUIMissingUiElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-missing-ui': UmbPropertyEditorUIMissingUiElement; + } +} 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 337dd9fe9e..2b3b948ffd 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,129 +1,17 @@ -import { css, customElement, html, nothing, property, query, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -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'; +import { UmbPropertyEditorUIMissingBaseElement } from './property-editor-ui-missing-base.element.js'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; /** * @element umb-property-editor-ui-missing */ @customElement('umb-property-editor-ui-missing') -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); - +export class UmbPropertyEditorUIMissingElement extends UmbPropertyEditorUIMissingBaseElement { constructor() { super(); - - 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); - }); - }); + this._titleKey = 'missingEditor_title'; + this._detailsDescriptionKey = 'missingEditor_detailsDescription'; + this._displayPropertyEditorUi = false; } - - private async _updateEditorAlias(dataType: UmbPropertyTypeModel['dataType']) { - this.observe(await this._dataTypeDetailRepository.byUnique(dataType.unique), (dataType) => { - this._dataTypeDetailModel = dataType; - }); - } - - async #onDetails() { - this._expanded = !this._expanded; - if (this._expanded) { - await this.updateComplete; - this.focalPointElement?.focus(); - } - } - - override render() { - return html` -
- ${this.localize.term('missingEditor_title')} -
-
- - ${this._expanded ? this._renderDetails() : nothing} -
- - - ${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/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts index b887ffbdde..7a1c506bb1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts @@ -74,8 +74,7 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement { #info-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(20%, 1fr)); - grid-gap: var(--uui-size-space-4); - max-width: 1000px; + grid-gap: var(--uui-size-space-5); } .info-link { diff --git a/src/Umbraco.Web.UI.Login/src/auth.element.ts b/src/Umbraco.Web.UI.Login/src/auth.element.ts index b8942fbcf4..d2fb5e1bc0 100644 --- a/src/Umbraco.Web.UI.Login/src/auth.element.ts +++ b/src/Umbraco.Web.UI.Login/src/auth.element.ts @@ -17,7 +17,6 @@ const createInput = (opts: { type: InputType; name: string; autocomplete: AutoFill; - label: string; inputmode: string; autofocus?: boolean; }) => { @@ -28,9 +27,7 @@ const createInput = (opts: { input.id = opts.id; input.required = true; input.inputMode = opts.inputmode; - input.ariaLabel = opts.label; input.autofocus = opts.autofocus || false; - return input; }; @@ -161,15 +158,11 @@ export default class UmbAuthElement extends UmbLitElement { * @private */ #initializeForm() { - const labelUsername = this.usernameIsEmail ? this.localize.term('auth_email') : this.localize.term('auth_username'); - const labelPassword = this.localize.term('auth_password'); - this._usernameInput = createInput({ id: 'username-input', type: 'text', name: 'username', autocomplete: 'username', - label: labelUsername, inputmode: this.usernameIsEmail ? 'email' : '', autofocus: true, }); @@ -178,7 +171,6 @@ export default class UmbAuthElement extends UmbLitElement { type: 'password', name: 'password', autocomplete: 'current-password', - label: labelPassword, inputmode: '', }); this._usernameLabel = createLabel({ diff --git a/templates/UmbracoDockerCompose/Database/Dockerfile b/templates/UmbracoDockerCompose/Database/Dockerfile index 4e74b6435e..de6ff234e1 100644 --- a/templates/UmbracoDockerCompose/Database/Dockerfile +++ b/templates/UmbracoDockerCompose/Database/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/azure-sql-edge:latest +FROM mcr.microsoft.com/mssql/server:2022-latest ENV ACCEPT_EULA=Y diff --git a/templates/UmbracoDockerCompose/Database/startup.sh b/templates/UmbracoDockerCompose/Database/startup.sh index c4fad8f0ae..ae3763b7d5 100644 --- a/templates/UmbracoDockerCompose/Database/startup.sh +++ b/templates/UmbracoDockerCompose/Database/startup.sh @@ -11,7 +11,7 @@ if [ "$1" = '/opt/mssql/bin/sqlservr' ]; then sleep 15s #run the setup script to create the DB and the schema in the DB - /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql -C # Note that the container has been initialized so future starts won't wipe changes to the data touch /tmp/app-initialized diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs index cb3414ad3a..acdda16406 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -1,13 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -22,6 +25,16 @@ internal sealed class DictionaryRepositoryTest : UmbracoIntegrationTest private IDictionaryRepository CreateRepository() => GetRequiredService(); + private IDictionaryRepository CreateRepositoryWithCache(AppCaches cache) => + + // Create a repository with a real runtime cache. + new DictionaryRepository( + GetRequiredService(), + cache, + GetRequiredService>(), + GetRequiredService(), + GetRequiredService()); + [Test] public async Task Can_Perform_Get_By_Key_On_DictionaryRepository() { @@ -396,6 +409,134 @@ internal sealed class DictionaryRepositoryTest : UmbracoIntegrationTest } } + [Test] + public void Can_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_With_Cache() + { + var cache = AppCaches.Create(Mock.Of()); + var repository = CreateRepositoryWithCache(cache); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + + // Modify the value directly in the database. This won't be reflected in the repository cache and hence if the cache + // is working as expected we should get the same value as above. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + + cache.IsolatedCaches.ClearCache(); + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + } + + [Test] + public void Can_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_With_Cache() + { + var cache = AppCaches.Create(Mock.Of()); + var repository = CreateRepositoryWithCache(cache); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNull(dictionaryItem); + } + + // Modify the value directly in the database such that it now exists. This won't be reflected in the repository cache and hence if the cache + // is working as expected we should get the same null value as above. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNull(dictionaryItem); + } + + cache.IsolatedCaches.ClearCache(); + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNotNull(dictionaryItem); + } + } + + [Test] + public void Cannot_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_Without_Cache() + { + var repository = CreateRepository(); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + + // Modify the value directly in the database. As we don't have caching enabled on the repository we should get the new value. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + } + + [Test] + public void Cannot_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_Without_Cache() + { + var repository = CreateRepository(); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNull(dictionaryItem); + } + + // Modify the value directly in the database such that it now exists. As we don't have caching enabled on the repository we should get the new value. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNotNull(dictionaryItem); + } + } + public async Task CreateTestData() { var languageService = GetRequiredService();