{"results":{"result":{"added-files":{"code-health":9.914185778642981,"old-code-health":0.0,"files":[{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/utils.ts","loc":40,"code-health":9.6882083290695},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/workspace/element-workspace-split-view-variant-selector.element.ts","loc":59,"code-health":9.6882083290695},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/modals/save-modal/element-save-modal.element.ts","loc":74,"code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/publishing/pending-changes/element-published-pending-changes.manager.test.ts","loc":195,"code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/publishing/pending-changes/element-published-pending-changes.manager.ts","loc":51,"code-health":9.6882083290695},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/folder/workspace/element-folder-menu-structure.context.ts","loc":23,"code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/collection/views/column-layouts/element-table-column-name.element.ts","loc":48,"code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/collection/views/column-layouts/element-table-column-state.element.ts","loc":55,"code-health":10.0}]},"external-review-url":"https://github.com/umbraco/Umbraco-CMS/pull/21897","old-code-health":9.337526264119182,"modified-files":{"code-health":9.059805906304245,"old-code-health":9.337526264119182,"files":[{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/repository/detail/element-detail.server.data-source.ts","loc":122,"old-loc":121,"code-health":10.0,"old-code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/workspace/element-workspace.context.ts","loc":220,"old-loc":161,"code-health":9.289613163071218,"old-code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/workspace/element-workspace-split-view.element.ts","loc":98,"old-loc":71,"code-health":10.0,"old-code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/publishing/workspace-context/element-publishing.workspace-context.ts","loc":356,"old-loc":292,"code-health":7.8674782998229595,"old-code-health":7.8674782998229595},{"file":"src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts","loc":171,"old-loc":153,"code-health":8.821672388351171,"old-code-health":9.3931346077612},{"file":"src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts","loc":88,"old-loc":101,"code-health":10.0,"old-code-health":10.0},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/tree/element.tree.server.data-source.ts","loc":73,"old-loc":72,"code-health":9.6882083290695,"old-code-health":9.6882083290695},{"file":"src/Umbraco.Web.UI.Client/src/packages/elements/collection/views/element-table-collection-view.element.ts","loc":126,"old-loc":143,"code-health":9.6882083290695,"old-code-health":10.0}]},"removed-files":{"code-health":0.0,"old-code-health":0.0,"files":[]},"external-review-id":"21897","analysis-time":"2026-04-07T08:21:28Z","negative-impact-count":7,"suppressions":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"affected-hotspots":0,"commits":["2f32e077d001e214009fefc36cb9a17ccfc400b8","144b39e727eca7b9d3349c17af3591e3e3de49f1","9082c5cf263ab32a4d3c3bf4bb868cb018052c05","2a200bb28fb2c4f6ed17ab4172ccc335616edafd","e860d3642a4c9277838f45c6660d8b3557e0ba54","e9853b31ee2c20e225c50a86a0a16e075d0df365","3b5e6897c439d4280290a01edd9e4aeeba251a83","6dcd3f4c88271ab26b92b34c981f8afbb056cc20","aa21076872a81a596934fcf43baef7c4e10c588c","eadb907de417172d3f6496aa14947ac952280fd3","7f2b3f1119fb924dd7e03afeab89f12a2c4a4c61","b3acf9126420238eb86fa02f02e3d313e3cfaa54","26238ce3ebaaafc00b742301fc2da0aacef2111a","7b9b80b9ed8cc591f2123450eeeb5d0a726e2ed6","551c20bb101e6a5fb2e35c5a73da9684563f32b0","982d9339a2db0c1815b94bc2a900a4bc930b2366","ca1319ca33a2a2db49f71f5ddd95aa0ed0af8694","d8aa271f1245715c97f4b6fe534326c1ab50b3f2","6bbd7c4ba030630ce4981df9c0e6a265dc142d7d","344fcd7e3b8287384a40ca700647a162849a6bda"],"is-negative-review":true,"negative-findings":{"number-of-types":5,"number-of-files-touched":6,"findings":[{"method":"UmbElementWorkspaceContext.constructor","why-it-occurs":"Overly long functions make the code harder to read. The recommended maximum function length for the TypeScript language is 70 lines of code. Severity: Brain Method - Complex Method - Long Method.","name":"Large Method","file":"src/Umbraco.Web.UI.Client/src/packages/elements/workspace/element-workspace.context.ts","refactoring-examples":null,"change-level":"warning","is-hotspot?":false,"line":59,"what-changed":"UmbElementWorkspaceContext.constructor has 74 lines, threshold = 70","how-to-fix":"We recommend to be careful here -- just splitting long functions don't necessarily make the code easier to read. Instead, look for natural chunks inside the functions that expresses a specific task or concern. Often, such concerns are indicated by a Code Comment followed by an if-statement. Use the [EXTRACT FUNCTION](https://refactoring.com/catalog/extractFunction.html) refactoring to encapsulate that concern.","change-type":"introduced"},{"why-it-occurs":"String is a generic type that fail to capture the constraints of the domain object it represents. In this module, 42 % of all function arguments are string types.","name":"String Heavy Function Arguments","file":"src/Umbraco.Web.UI.Client/src/packages/elements/workspace/element-workspace.context.ts","refactoring-examples":null,"change-level":"warning","is-hotspot?":false,"what-changed":"In this module, 41.7% of all arguments to its 13 functions are strings. The threshold for string arguments is 39.0%","how-to-fix":"Heavy string usage indicates a missing domain language. Introduce data types that encapsulate the semantics. For example, a user_name is better represented as a constrained User type rather than a pure string, which could be anything.","change-type":"introduced"},{"method":"compareMandatory","why-it-occurs":"A Complex Method has a high cyclomatic complexity. The recommended threshold for the TypeScript language is a cyclomatic complexity lower than 9.","name":"Complex Method","file":"src/Umbraco.Web.UI.Client/src/packages/elements/utils.ts","refactoring-examples":[{"architectural-component-id":null,"author-name":"Niels Lyngsø","training-data":{"loc-added":"9","loc-deleted":"10","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"nsl@umbraco.dk","commit-full-message":"","commit-date":"2025-02-17T08:11:28Z","current-rev":"8bbe0533c3","filename":"Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts","previous-rev":"5379fec2d3","commit-title":"Close active modal if its begin unregistered (#18285)","language":"TypeScript","id":"88aeef715fd9d2fa61f2042a7a9017a6bab23d00","model-score":0.6,"author-id":null,"project-id":33308,"delta-file-score":0.31179166,"diff":"diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\nindex 96d25919bf..aad4a291a0 100644\n--- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\n+++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\n@@ -1,2 +1,2 @@\n-import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '../types.js';\n+import type { UmbContentTypeModel } from '../types.js';\n import type { UmbContentTypeStructureManager } from './content-type-structure-manager.class.js';\n@@ -12,3 +12,2 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t#structure?: UmbContentTypeStructureManager<T>;\n-\t#tabContainers?: Array<UmbPropertyTypeContainerModel>;\n \n@@ -20,12 +19,10 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \n-\t#observeContainers() {\n+\tasync #observeContainers() {\n \t\tif (!this.#structure) return;\n \n-\t\tthis.observe(\n+\t\tawait this.observe(\n \t\t\tthis.#structure.ownerContainersOf('Tab', null),\n \t\t\t(tabContainers) => {\n-\t\t\t\tconst old = this.#tabContainers;\n-\t\t\t\tthis.#tabContainers = tabContainers;\n-\t\t\t\t// If the amount of containers was 0 before and now becomes 1, we should move all root containers into this tab:\n-\t\t\t\tif (old?.length === 0 && tabContainers?.length === 1) {\n+\t\t\t\t// If the amount of containers now became 1, we should move all root containers into this tab:\n+\t\t\t\tif (tabContainers?.length === 1) {\n \t\t\t\t\tconst firstTabId = tabContainers[0].id;\n@@ -35,2 +32,3 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\t\t\t\t});\n+\t\t\t\t\tthis.destroy();\n \t\t\t\t}\n@@ -38,3 +36,5 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\t\t'_observeMainContainer',\n-\t\t);\n+\t\t).asPromise();\n+\n+\t\tthis.destroy();\n \t}\n@@ -44,3 +44,2 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\tthis.#structure = undefined;\n-\t\tthis.#tabContainers = undefined;\n \t}\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Niels Lyngsø","training-data":{"loc-added":"54","loc-deleted":"72","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"nsl@umbraco.dk","commit-full-message":"* improve sorting algorithm\n\n* fix block type input\n\n* make confirm modal localizable\n\n* rename method\n\n* clean up\n\n* clean up\n\n* improve code\n\n* Fix creating Block Types in Groups\n\n* remove #moveData\n\n* lint fixes\n\n* remove unused\n\n---------\n\nCo-authored-by: Mads Rasmussen <madsr@hey.com>","commit-date":"2025-01-20T08:10:50Z","current-rev":"6c1c851d8a","filename":"Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts","previous-rev":"4353027655","commit-title":"Fix: Improve sorter placement algorithm (#18021)","language":"TypeScript","id":"1c09c6d9203e7223417b741738479d2b893aaab9","model-score":0.27,"author-id":null,"project-id":33308,"delta-file-score":0.31179166,"diff":"diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\nindex ae743a577d..9c3f2f9276 100644\n--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\n+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\n@@ -1,7 +1,5 @@\n-import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '../../../block-type/index.js';\n-import '../../../block-type/components/input-block-type/index.js';\n-import {\n-\ttype UmbPropertyEditorUiElement,\n-\tUmbPropertyValueChangeEvent,\n-\ttype UmbPropertyEditorConfigCollection,\n+import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type';\n+import type {\n+\tUmbPropertyEditorUiElement,\n+\tUmbPropertyEditorConfigCollection,\n } from '@umbraco-cms/backoffice/property-editor';\n@@ -32,2 +30,4 @@ import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/rou\n import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';\n+import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';\n+import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';\n \n@@ -45,3 +45,2 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n {\n-\t#moveData?: Array<UmbBlockTypeWithGroupKey>;\n \t#sorter = new UmbSorterController<MappedGroupWithBlockTypes, HTMLElement>(this, {\n@@ -106,4 +105,10 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\tthis.#datasetContext = context;\n-\t\t\t//this.#observeBlocks();\n-\t\t\tthis.#observeBlockGroups();\n+\t\t\tthis.observe(\n+\t\t\t\tawait this.#datasetContext.propertyValueByAlias('blockGroups'),\n+\t\t\t\t(value) => {\n+\t\t\t\t\tthis.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];\n+\t\t\t\t\tthis.#mapValuesToBlockGroups();\n+\t\t\t\t},\n+\t\t\t\t'_observeBlockGroups',\n+\t\t\t);\n \t\t});\n@@ -121,20 +126,2 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \n-\tasync #observeBlockGroups() {\n-\t\tif (!this.#datasetContext) return;\n-\t\tthis.observe(await this.#datasetContext.propertyValueByAlias('blockGroups'), (value) => {\n-\t\t\tthis.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];\n-\t\t\tthis.#mapValuesToBlockGroups();\n-\t\t});\n-\t}\n-\t// TODO: No need for this, we just got the value via the value property.. [NL]\n-\t/*\n-\tasync #observeBlocks() {\n-\t\tif (!this.#datasetContext) return;\n-\t\tthis.observe(await this.#datasetContext.propertyValueByAlias('blocks'), (value) => {\n-\t\t\tthis.value = (value as Array<UmbBlockTypeWithGroupKey>) ?? [];\n-\t\t\tthis.#mapValuesToBlockGroups();\n-\t\t});\n-\t}\n-\t*/\n-\n \t#mapValuesToBlockGroups() {\n@@ -154,36 +141,30 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \n-\t#onDelete(e: CustomEvent, groupKey?: string) {\n-\t\tconst updatedValues = (e.target as UmbInputBlockTypeElement).value.map((value) => ({ ...value, groupKey }));\n-\t\tconst filteredValues = this.#value.filter((value) => value.groupKey !== groupKey);\n-\t\tthis.value = [...filteredValues, ...updatedValues];\n-\t\tthis.dispatchEvent(new UmbPropertyValueChangeEvent());\n-\t}\n-\n-\tasync #onChange(e: CustomEvent) {\n+\tasync #onChange(e: Event, groupKey?: string) {\n \t\te.stopPropagation();\n \t\tconst element = e.target as UmbInputBlockTypeElement;\n-\t\tconst value = element.value;\n-\n-\t\tif (!e.detail?.moveComplete) {\n-\t\t\t// Container change, store data of the new group...\n-\t\t\tconst newGroupKey = element.getAttribute('data-umb-group-key');\n-\t\t\tconst movedItem = e.detail?.item as UmbBlockTypeWithGroupKey;\n-\t\t\t// Check if item moved back to original group...\n-\t\t\tif (movedItem.groupKey === newGroupKey) {\n-\t\t\t\tthis.#moveData = undefined;\n-\t\t\t} else {\n-\t\t\t\tthis.#moveData = value.map((block) => ({ ...block, groupKey: newGroupKey }));\n-\t\t\t}\n-\t\t} else if (e.detail?.moveComplete) {\n-\t\t\t// Move complete, get the blocks that were in an untouched group\n-\t\t\tconst blocks = this.#value\n-\t\t\t\t.filter((block) => !value.find((value) => value.contentElementTypeKey === block.contentElementTypeKey))\n-\t\t\t\t.filter(\n-\t\t\t\t\t(block) => !this.#moveData?.find((value) => value.contentElementTypeKey === block.contentElementTypeKey),\n-\t\t\t\t);\n-\n-\t\t\tthis.value = this.#moveData ? [...blocks, ...value, ...this.#moveData] : [...blocks, ...value];\n-\t\t\tthis.dispatchEvent(new UmbPropertyValueChangeEvent());\n-\t\t\tthis.#moveData = undefined;\n+\t\tconst value = element.value.map((x) => ({ ...x, groupKey }));\n+\n+\t\tif (groupKey) {\n+\t\t\t// Update the specific group:\n+\t\t\tthis._groupsWithBlockTypes = this._groupsWithBlockTypes.map((group) => {\n+\t\t\t\tif (group.key === groupKey) {\n+\t\t\t\t\treturn { ...group, blocks: value };\n+\t\t\t\t}\n+\t\t\t\treturn group;\n+\t\t\t});\n+\t\t} else {\n+\t\t\t// Update the not grouped blocks:\n+\t\t\tthis._notGroupedBlockTypes = value;\n \t\t}\n+\n+\t\tthis.#updateValue();\n+\t}\n+\n+\t#updateValue() {\n+\t\tthis.value = [...this._notGroupedBlockTypes, ...this._groupsWithBlockTypes.flatMap((group) => group.blocks)];\n+\t\tthis.dispatchEvent(new UmbChangeEvent());\n+\t}\n+\n+\t#updateBlockGroupsValue(groups: Array<UmbBlockGridTypeGroupType>) {\n+\t\tthis.#datasetContext?.setPropertyValue('blockGroups', groups);\n \t}\n@@ -193,3 +174,3 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\tif (selectedElementType) {\n-\t\t\tthis.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? null));\n+\t\t\tthis.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? 'null'));\n \t\t}\n@@ -198,15 +179,18 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t// TODO: Implement confirm dialog [NL]\n-\t#deleteGroup(groupKey: string) {\n-\t\t// TODO: make one method for updating the blockGroupsDataSetValue: [NL]\n-\t\t// This one that deletes might require the ability to parse what to send as an argument to the method, then a filtering can occur before.\n-\t\tthis.#datasetContext?.setPropertyValue(\n-\t\t\t'blockGroups',\n-\t\t\tthis.#blockGroups?.filter((group) => group.key !== groupKey),\n-\t\t);\n-\n+\tasync #deleteGroup(groupKey: string) {\n+\t\tconst groupName = this.#blockGroups?.find((group) => group.key === groupKey)?.name ?? '';\n+\t\tawait umbConfirmModal(this, {\n+\t\t\theadline: '#blockEditor_confirmDeleteBlockGroupTitle',\n+\t\t\tcontent: this.localize.term('#blockEditor_confirmDeleteBlockGroupMessage', [groupName]),\n+\t\t\tcolor: 'danger',\n+\t\t\tconfirmLabel: '#general_delete',\n+\t\t});\n \t\t// If a group is deleted, Move the blocks to no group:\n \t\tthis.value = this.#value.map((block) => (block.groupKey === groupKey ? { ...block, groupKey: undefined } : block));\n+\t\tif (this.#blockGroups) {\n+\t\t\tthis.#updateBlockGroupsValue(this.#blockGroups.filter((group) => group.key !== groupKey));\n+\t\t}\n \t}\n \n-\t#changeGroupName(e: UUIInputEvent, groupKey: string) {\n+\t#onGroupNameChange(e: UUIInputEvent, groupKey: string) {\n \t\tconst groupName = e.target.value as string;\n@@ -226,5 +210,4 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t\t\t.workspacePath=${this._workspacePath}\n-\t\t\t\t\t\t@change=${this.#onChange}\n-\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}\n-\t\t\t\t\t\t@delete=${(e: CustomEvent) => this.#onDelete(e, undefined)}></umb-input-block-type>`\n+\t\t\t\t\t\t@change=${(e: Event) => this.#onChange(e, undefined)}\n+\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}></umb-input-block-type>`\n \t\t\t\t: ''}\n@@ -241,5 +224,4 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t\t\t\t.workspacePath=${this._workspacePath}\n-\t\t\t\t\t\t\t@change=${this.#onChange}\n-\t\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}\n-\t\t\t\t\t\t\t@delete=${(e: CustomEvent) => this.#onDelete(e, group.key)}></umb-input-block-type>\n+\t\t\t\t\t\t\t@change=${(e: Event) => this.#onChange(e, group.key)}\n+\t\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}></umb-input-block-type>\n \t\t\t\t\t</div>`,\n@@ -255,3 +237,3 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t.value=${groupName ?? ''}\n-\t\t\t\t@change=${(e: UUIInputEvent) => this.#changeGroupName(e, groupKey)}>\n+\t\t\t\t@change=${(e: UUIInputEvent) => this.#onGroupNameChange(e, groupKey)}>\n \t\t\t\t<uui-button compact slot=\"append\" label=\"delete\" @click=${() => this.#deleteGroup(groupKey)}>\n","improvement-type":"Complex Method"}],"change-level":"warning","is-hotspot?":false,"line":31,"what-changed":"compareMandatory has a cyclomatic complexity of 9, threshold = 9","how-to-fix":"There are many reasons for Complex Method. Sometimes, another design approach is beneficial such as a) modeling state using an explicit state machine rather than conditionals, or b) using table lookup rather than long chains of logic. In other scenarios, the function can be split using [EXTRACT FUNCTION](https://refactoring.com/catalog/extractFunction.html). Just make sure you extract natural and cohesive functions. Complex Methods can also be addressed by identifying complex conditional expressions and then using the [DECOMPOSE CONDITIONAL](https://refactoring.com/catalog/decomposeConditional.html) refactoring.","change-type":"introduced"},{"method":"UmbElementWorkspaceSplitViewVariantSelectorElement.getVariantState","why-it-occurs":"A complex conditional is an expression inside a branch such as an <code>if</code>-statmeent which consists of multiple, logical operations. Example: <code>if (x.started() && y.running())</code>.Complex conditionals make the code even harder to read, and contribute to the Complex Method code smell. Encapsulate them.","name":"Complex Conditional","file":"src/Umbraco.Web.UI.Client/src/packages/elements/workspace/element-workspace-split-view-variant-selector.element.ts","refactoring-examples":[{"diff":"diff --git a/complex_conditional.js b/complex_conditional.js\nindex c43da09584..94259ce874 100644\n--- a/complex_conditional.js\n+++ b/complex_conditional.js\n@@ -1,16 +1,34 @@\n function messageReceived(message, timeReceived) {\n-   // Ignore all messages which aren't from known customers:\n-   if (!message.sender &&\n-       customers.getId(message.name) == null) {\n+   // Refactoring #1: encapsulate the business rule in a\n+   // function. A clear name replaces the need for the comment:\n+   if (!knownCustomer(message)) {\n      log('spam received -- ignoring');\n      return;\n    }\n \n-  // Provide an auto-reply when outside business hours:\n-  if ((timeReceived.getHours() > 17) ||\n-      (timeReceived.getHours() < 8)) {\n+  // Refactoring #2: encapsulate the business rule.\n+  // Again, note how a clear function name replaces the\n+  // need for a code comment:\n+  if (outsideBusinessHours(timeReceived)) {\n     return autoReplyTo(message);\n   }\n \n   pingAgentFor(message);\n+}\n+\n+function outsideBusinessHours(timeReceived) {\n+  // Refactoring #3: replace magic numbers with\n+  // symbols that communicate with the code reader:\n+  const closingHour = 17;\n+  const openingHour = 8;\n+\n+  const hours = timeReceived.getHours();\n+\n+  // Refactoring #4: simple conditional rules can\n+  // be further clarified by introducing a variable:\n+  const afterClosing = hours > closingHour;\n+  const beforeOpening = hours < openingHour;\n+\n+  // Yeah -- look how clear the business rule is now!\n+  return afterClosing || beforeOpening;\n }\n\\ No newline at end of file\n","language":"typescript","improvement-type":"Complex Conditional"}],"change-level":"warning","is-hotspot?":false,"line":54,"what-changed":"UmbElementWorkspaceSplitViewVariantSelectorElement.getVariantState has 1 complex conditionals with 2 branches, threshold = 2","how-to-fix":"Apply the [DECOMPOSE CONDITIONAL](https://refactoring.com/catalog/decomposeConditional.html) refactoring so that the complex conditional is encapsulated in a separate function with a good name that captures the business rule. Optionally, for simple expressions, introduce a new variable which holds the result of the complex conditional.","change-type":"introduced"},{"method":"UmbElementPublishedPendingChangesManager.process","why-it-occurs":"A Complex Method has a high cyclomatic complexity. The recommended threshold for the TypeScript language is a cyclomatic complexity lower than 9.","name":"Complex Method","file":"src/Umbraco.Web.UI.Client/src/packages/elements/publishing/pending-changes/element-published-pending-changes.manager.ts","refactoring-examples":[{"architectural-component-id":null,"author-name":"Niels Lyngsø","training-data":{"loc-added":"9","loc-deleted":"10","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"nsl@umbraco.dk","commit-full-message":"","commit-date":"2025-02-17T08:11:28Z","current-rev":"8bbe0533c3","filename":"Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts","previous-rev":"5379fec2d3","commit-title":"Close active modal if its begin unregistered (#18285)","language":"TypeScript","id":"88aeef715fd9d2fa61f2042a7a9017a6bab23d00","model-score":0.6,"author-id":null,"project-id":33308,"delta-file-score":0.31179166,"diff":"diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\nindex 96d25919bf..aad4a291a0 100644\n--- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\n+++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\n@@ -1,2 +1,2 @@\n-import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '../types.js';\n+import type { UmbContentTypeModel } from '../types.js';\n import type { UmbContentTypeStructureManager } from './content-type-structure-manager.class.js';\n@@ -12,3 +12,2 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t#structure?: UmbContentTypeStructureManager<T>;\n-\t#tabContainers?: Array<UmbPropertyTypeContainerModel>;\n \n@@ -20,12 +19,10 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \n-\t#observeContainers() {\n+\tasync #observeContainers() {\n \t\tif (!this.#structure) return;\n \n-\t\tthis.observe(\n+\t\tawait this.observe(\n \t\t\tthis.#structure.ownerContainersOf('Tab', null),\n \t\t\t(tabContainers) => {\n-\t\t\t\tconst old = this.#tabContainers;\n-\t\t\t\tthis.#tabContainers = tabContainers;\n-\t\t\t\t// If the amount of containers was 0 before and now becomes 1, we should move all root containers into this tab:\n-\t\t\t\tif (old?.length === 0 && tabContainers?.length === 1) {\n+\t\t\t\t// If the amount of containers now became 1, we should move all root containers into this tab:\n+\t\t\t\tif (tabContainers?.length === 1) {\n \t\t\t\t\tconst firstTabId = tabContainers[0].id;\n@@ -35,2 +32,3 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\t\t\t\t});\n+\t\t\t\t\tthis.destroy();\n \t\t\t\t}\n@@ -38,3 +36,5 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\t\t'_observeMainContainer',\n-\t\t);\n+\t\t).asPromise();\n+\n+\t\tthis.destroy();\n \t}\n@@ -44,3 +44,2 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\tthis.#structure = undefined;\n-\t\tthis.#tabContainers = undefined;\n \t}\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Niels Lyngsø","training-data":{"loc-added":"54","loc-deleted":"72","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"nsl@umbraco.dk","commit-full-message":"* improve sorting algorithm\n\n* fix block type input\n\n* make confirm modal localizable\n\n* rename method\n\n* clean up\n\n* clean up\n\n* improve code\n\n* Fix creating Block Types in Groups\n\n* remove #moveData\n\n* lint fixes\n\n* remove unused\n\n---------\n\nCo-authored-by: Mads Rasmussen <madsr@hey.com>","commit-date":"2025-01-20T08:10:50Z","current-rev":"6c1c851d8a","filename":"Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts","previous-rev":"4353027655","commit-title":"Fix: Improve sorter placement algorithm (#18021)","language":"TypeScript","id":"1c09c6d9203e7223417b741738479d2b893aaab9","model-score":0.27,"author-id":null,"project-id":33308,"delta-file-score":0.31179166,"diff":"diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\nindex ae743a577d..9c3f2f9276 100644\n--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\n+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\n@@ -1,7 +1,5 @@\n-import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '../../../block-type/index.js';\n-import '../../../block-type/components/input-block-type/index.js';\n-import {\n-\ttype UmbPropertyEditorUiElement,\n-\tUmbPropertyValueChangeEvent,\n-\ttype UmbPropertyEditorConfigCollection,\n+import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type';\n+import type {\n+\tUmbPropertyEditorUiElement,\n+\tUmbPropertyEditorConfigCollection,\n } from '@umbraco-cms/backoffice/property-editor';\n@@ -32,2 +30,4 @@ import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/rou\n import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';\n+import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';\n+import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';\n \n@@ -45,3 +45,2 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n {\n-\t#moveData?: Array<UmbBlockTypeWithGroupKey>;\n \t#sorter = new UmbSorterController<MappedGroupWithBlockTypes, HTMLElement>(this, {\n@@ -106,4 +105,10 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\tthis.#datasetContext = context;\n-\t\t\t//this.#observeBlocks();\n-\t\t\tthis.#observeBlockGroups();\n+\t\t\tthis.observe(\n+\t\t\t\tawait this.#datasetContext.propertyValueByAlias('blockGroups'),\n+\t\t\t\t(value) => {\n+\t\t\t\t\tthis.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];\n+\t\t\t\t\tthis.#mapValuesToBlockGroups();\n+\t\t\t\t},\n+\t\t\t\t'_observeBlockGroups',\n+\t\t\t);\n \t\t});\n@@ -121,20 +126,2 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \n-\tasync #observeBlockGroups() {\n-\t\tif (!this.#datasetContext) return;\n-\t\tthis.observe(await this.#datasetContext.propertyValueByAlias('blockGroups'), (value) => {\n-\t\t\tthis.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];\n-\t\t\tthis.#mapValuesToBlockGroups();\n-\t\t});\n-\t}\n-\t// TODO: No need for this, we just got the value via the value property.. [NL]\n-\t/*\n-\tasync #observeBlocks() {\n-\t\tif (!this.#datasetContext) return;\n-\t\tthis.observe(await this.#datasetContext.propertyValueByAlias('blocks'), (value) => {\n-\t\t\tthis.value = (value as Array<UmbBlockTypeWithGroupKey>) ?? [];\n-\t\t\tthis.#mapValuesToBlockGroups();\n-\t\t});\n-\t}\n-\t*/\n-\n \t#mapValuesToBlockGroups() {\n@@ -154,36 +141,30 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \n-\t#onDelete(e: CustomEvent, groupKey?: string) {\n-\t\tconst updatedValues = (e.target as UmbInputBlockTypeElement).value.map((value) => ({ ...value, groupKey }));\n-\t\tconst filteredValues = this.#value.filter((value) => value.groupKey !== groupKey);\n-\t\tthis.value = [...filteredValues, ...updatedValues];\n-\t\tthis.dispatchEvent(new UmbPropertyValueChangeEvent());\n-\t}\n-\n-\tasync #onChange(e: CustomEvent) {\n+\tasync #onChange(e: Event, groupKey?: string) {\n \t\te.stopPropagation();\n \t\tconst element = e.target as UmbInputBlockTypeElement;\n-\t\tconst value = element.value;\n-\n-\t\tif (!e.detail?.moveComplete) {\n-\t\t\t// Container change, store data of the new group...\n-\t\t\tconst newGroupKey = element.getAttribute('data-umb-group-key');\n-\t\t\tconst movedItem = e.detail?.item as UmbBlockTypeWithGroupKey;\n-\t\t\t// Check if item moved back to original group...\n-\t\t\tif (movedItem.groupKey === newGroupKey) {\n-\t\t\t\tthis.#moveData = undefined;\n-\t\t\t} else {\n-\t\t\t\tthis.#moveData = value.map((block) => ({ ...block, groupKey: newGroupKey }));\n-\t\t\t}\n-\t\t} else if (e.detail?.moveComplete) {\n-\t\t\t// Move complete, get the blocks that were in an untouched group\n-\t\t\tconst blocks = this.#value\n-\t\t\t\t.filter((block) => !value.find((value) => value.contentElementTypeKey === block.contentElementTypeKey))\n-\t\t\t\t.filter(\n-\t\t\t\t\t(block) => !this.#moveData?.find((value) => value.contentElementTypeKey === block.contentElementTypeKey),\n-\t\t\t\t);\n-\n-\t\t\tthis.value = this.#moveData ? [...blocks, ...value, ...this.#moveData] : [...blocks, ...value];\n-\t\t\tthis.dispatchEvent(new UmbPropertyValueChangeEvent());\n-\t\t\tthis.#moveData = undefined;\n+\t\tconst value = element.value.map((x) => ({ ...x, groupKey }));\n+\n+\t\tif (groupKey) {\n+\t\t\t// Update the specific group:\n+\t\t\tthis._groupsWithBlockTypes = this._groupsWithBlockTypes.map((group) => {\n+\t\t\t\tif (group.key === groupKey) {\n+\t\t\t\t\treturn { ...group, blocks: value };\n+\t\t\t\t}\n+\t\t\t\treturn group;\n+\t\t\t});\n+\t\t} else {\n+\t\t\t// Update the not grouped blocks:\n+\t\t\tthis._notGroupedBlockTypes = value;\n \t\t}\n+\n+\t\tthis.#updateValue();\n+\t}\n+\n+\t#updateValue() {\n+\t\tthis.value = [...this._notGroupedBlockTypes, ...this._groupsWithBlockTypes.flatMap((group) => group.blocks)];\n+\t\tthis.dispatchEvent(new UmbChangeEvent());\n+\t}\n+\n+\t#updateBlockGroupsValue(groups: Array<UmbBlockGridTypeGroupType>) {\n+\t\tthis.#datasetContext?.setPropertyValue('blockGroups', groups);\n \t}\n@@ -193,3 +174,3 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\tif (selectedElementType) {\n-\t\t\tthis.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? null));\n+\t\t\tthis.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? 'null'));\n \t\t}\n@@ -198,15 +179,18 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t// TODO: Implement confirm dialog [NL]\n-\t#deleteGroup(groupKey: string) {\n-\t\t// TODO: make one method for updating the blockGroupsDataSetValue: [NL]\n-\t\t// This one that deletes might require the ability to parse what to send as an argument to the method, then a filtering can occur before.\n-\t\tthis.#datasetContext?.setPropertyValue(\n-\t\t\t'blockGroups',\n-\t\t\tthis.#blockGroups?.filter((group) => group.key !== groupKey),\n-\t\t);\n-\n+\tasync #deleteGroup(groupKey: string) {\n+\t\tconst groupName = this.#blockGroups?.find((group) => group.key === groupKey)?.name ?? '';\n+\t\tawait umbConfirmModal(this, {\n+\t\t\theadline: '#blockEditor_confirmDeleteBlockGroupTitle',\n+\t\t\tcontent: this.localize.term('#blockEditor_confirmDeleteBlockGroupMessage', [groupName]),\n+\t\t\tcolor: 'danger',\n+\t\t\tconfirmLabel: '#general_delete',\n+\t\t});\n \t\t// If a group is deleted, Move the blocks to no group:\n \t\tthis.value = this.#value.map((block) => (block.groupKey === groupKey ? { ...block, groupKey: undefined } : block));\n+\t\tif (this.#blockGroups) {\n+\t\t\tthis.#updateBlockGroupsValue(this.#blockGroups.filter((group) => group.key !== groupKey));\n+\t\t}\n \t}\n \n-\t#changeGroupName(e: UUIInputEvent, groupKey: string) {\n+\t#onGroupNameChange(e: UUIInputEvent, groupKey: string) {\n \t\tconst groupName = e.target.value as string;\n@@ -226,5 +210,4 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t\t\t.workspacePath=${this._workspacePath}\n-\t\t\t\t\t\t@change=${this.#onChange}\n-\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}\n-\t\t\t\t\t\t@delete=${(e: CustomEvent) => this.#onDelete(e, undefined)}></umb-input-block-type>`\n+\t\t\t\t\t\t@change=${(e: Event) => this.#onChange(e, undefined)}\n+\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}></umb-input-block-type>`\n \t\t\t\t: ''}\n@@ -241,5 +224,4 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t\t\t\t.workspacePath=${this._workspacePath}\n-\t\t\t\t\t\t\t@change=${this.#onChange}\n-\t\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}\n-\t\t\t\t\t\t\t@delete=${(e: CustomEvent) => this.#onDelete(e, group.key)}></umb-input-block-type>\n+\t\t\t\t\t\t\t@change=${(e: Event) => this.#onChange(e, group.key)}\n+\t\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}></umb-input-block-type>\n \t\t\t\t\t</div>`,\n@@ -255,3 +237,3 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t.value=${groupName ?? ''}\n-\t\t\t\t@change=${(e: UUIInputEvent) => this.#changeGroupName(e, groupKey)}>\n+\t\t\t\t@change=${(e: UUIInputEvent) => this.#onGroupNameChange(e, groupKey)}>\n \t\t\t\t<uui-button compact slot=\"append\" label=\"delete\" @click=${() => this.#deleteGroup(groupKey)}>\n","improvement-type":"Complex Method"}],"change-level":"warning","is-hotspot?":false,"line":31,"what-changed":"UmbElementPublishedPendingChangesManager.process has a cyclomatic complexity of 9, threshold = 9","how-to-fix":"There are many reasons for Complex Method. Sometimes, another design approach is beneficial such as a) modeling state using an explicit state machine rather than conditionals, or b) using table lookup rather than long chains of logic. In other scenarios, the function can be split using [EXTRACT FUNCTION](https://refactoring.com/catalog/extractFunction.html). Just make sure you extract natural and cohesive functions. Complex Methods can also be addressed by identifying complex conditional expressions and then using the [DECOMPOSE CONDITIONAL](https://refactoring.com/catalog/decomposeConditional.html) refactoring.","change-type":"introduced"},{"why-it-occurs":"Overall Code Complexity is measured by the mean cyclomatic complexity across all functions in the file. The lower the number, the better.\n\nCyclomatic complexity is a function level metric that measures the number of logical branches (if-else, loops, etc.). Cyclomatic complexity is a rough complexity measure, but useful as a way of estimating the minimum number of unit tests you would need. As such, prefer functions with low cyclomatic complexity (2-3 branches).","name":"Overall Code Complexity","file":"src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts","refactoring-examples":null,"change-level":"warning","is-hotspot?":false,"what-changed":"This module has a mean cyclomatic complexity of 6.00 across 7 functions. The mean complexity threshold is 4","how-to-fix":"You address the overall cyclomatic complexity by a) modularizing the code, and b) abstract away the complexity. Let's look at some examples:\n\nModularizing the Code: Do an X-Ray and inspect the local hotspots. Are there any complex conditional expressions? If yes, then do a [DECOMPOSE CONDITIONAL](https://refactoring.com/catalog/decomposeConditional.html) refactoring. Extract the conditional logic into a separate function and put a good name on that function. This clarifies the intent and makes the original function easier to read. Repeat until all complex conditional expressions have been simplified.\n\n","change-type":"introduced"},{"method":"UmbElementTableCollectionViewElement.createTableItems","why-it-occurs":"A Complex Method has a high cyclomatic complexity. The recommended threshold for the TypeScript language is a cyclomatic complexity lower than 9.","name":"Complex Method","file":"src/Umbraco.Web.UI.Client/src/packages/elements/collection/views/element-table-collection-view.element.ts","refactoring-examples":[{"architectural-component-id":null,"author-name":"Niels Lyngsø","training-data":{"loc-added":"9","loc-deleted":"10","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"nsl@umbraco.dk","commit-full-message":"","commit-date":"2025-02-17T08:11:28Z","current-rev":"8bbe0533c3","filename":"Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts","previous-rev":"5379fec2d3","commit-title":"Close active modal if its begin unregistered (#18285)","language":"TypeScript","id":"88aeef715fd9d2fa61f2042a7a9017a6bab23d00","model-score":0.6,"author-id":null,"project-id":33308,"delta-file-score":0.31179166,"diff":"diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\nindex 96d25919bf..aad4a291a0 100644\n--- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\n+++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-move-root-containers-into-first-tab-helper.class.ts\n@@ -1,2 +1,2 @@\n-import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '../types.js';\n+import type { UmbContentTypeModel } from '../types.js';\n import type { UmbContentTypeStructureManager } from './content-type-structure-manager.class.js';\n@@ -12,3 +12,2 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t#structure?: UmbContentTypeStructureManager<T>;\n-\t#tabContainers?: Array<UmbPropertyTypeContainerModel>;\n \n@@ -20,12 +19,10 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \n-\t#observeContainers() {\n+\tasync #observeContainers() {\n \t\tif (!this.#structure) return;\n \n-\t\tthis.observe(\n+\t\tawait this.observe(\n \t\t\tthis.#structure.ownerContainersOf('Tab', null),\n \t\t\t(tabContainers) => {\n-\t\t\t\tconst old = this.#tabContainers;\n-\t\t\t\tthis.#tabContainers = tabContainers;\n-\t\t\t\t// If the amount of containers was 0 before and now becomes 1, we should move all root containers into this tab:\n-\t\t\t\tif (old?.length === 0 && tabContainers?.length === 1) {\n+\t\t\t\t// If the amount of containers now became 1, we should move all root containers into this tab:\n+\t\t\t\tif (tabContainers?.length === 1) {\n \t\t\t\t\tconst firstTabId = tabContainers[0].id;\n@@ -35,2 +32,3 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\t\t\t\t});\n+\t\t\t\t\tthis.destroy();\n \t\t\t\t}\n@@ -38,3 +36,5 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\t\t'_observeMainContainer',\n-\t\t);\n+\t\t).asPromise();\n+\n+\t\tthis.destroy();\n \t}\n@@ -44,3 +44,2 @@ export class UmbContentTypeMoveRootGroupsIntoFirstTabHelper<T extends UmbContent\n \t\tthis.#structure = undefined;\n-\t\tthis.#tabContainers = undefined;\n \t}\n","improvement-type":"Complex Method"},{"architectural-component-id":null,"author-name":"Niels Lyngsø","training-data":{"loc-added":"54","loc-deleted":"72","delta-cc-mean":"0.0","delta-cc-total":"0","delta-penalties":"1.0","delta-n-functions":"0","current-file-score":"10.0"},"author-email":"nsl@umbraco.dk","commit-full-message":"* improve sorting algorithm\n\n* fix block type input\n\n* make confirm modal localizable\n\n* rename method\n\n* clean up\n\n* clean up\n\n* improve code\n\n* Fix creating Block Types in Groups\n\n* remove #moveData\n\n* lint fixes\n\n* remove unused\n\n---------\n\nCo-authored-by: Mads Rasmussen <madsr@hey.com>","commit-date":"2025-01-20T08:10:50Z","current-rev":"6c1c851d8a","filename":"Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts","previous-rev":"4353027655","commit-title":"Fix: Improve sorter placement algorithm (#18021)","language":"TypeScript","id":"1c09c6d9203e7223417b741738479d2b893aaab9","model-score":0.27,"author-id":null,"project-id":33308,"delta-file-score":0.31179166,"diff":"diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\nindex ae743a577d..9c3f2f9276 100644\n--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\n+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts\n@@ -1,7 +1,5 @@\n-import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '../../../block-type/index.js';\n-import '../../../block-type/components/input-block-type/index.js';\n-import {\n-\ttype UmbPropertyEditorUiElement,\n-\tUmbPropertyValueChangeEvent,\n-\ttype UmbPropertyEditorConfigCollection,\n+import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type';\n+import type {\n+\tUmbPropertyEditorUiElement,\n+\tUmbPropertyEditorConfigCollection,\n } from '@umbraco-cms/backoffice/property-editor';\n@@ -32,2 +30,4 @@ import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/rou\n import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';\n+import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';\n+import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';\n \n@@ -45,3 +45,2 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n {\n-\t#moveData?: Array<UmbBlockTypeWithGroupKey>;\n \t#sorter = new UmbSorterController<MappedGroupWithBlockTypes, HTMLElement>(this, {\n@@ -106,4 +105,10 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\tthis.#datasetContext = context;\n-\t\t\t//this.#observeBlocks();\n-\t\t\tthis.#observeBlockGroups();\n+\t\t\tthis.observe(\n+\t\t\t\tawait this.#datasetContext.propertyValueByAlias('blockGroups'),\n+\t\t\t\t(value) => {\n+\t\t\t\t\tthis.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];\n+\t\t\t\t\tthis.#mapValuesToBlockGroups();\n+\t\t\t\t},\n+\t\t\t\t'_observeBlockGroups',\n+\t\t\t);\n \t\t});\n@@ -121,20 +126,2 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \n-\tasync #observeBlockGroups() {\n-\t\tif (!this.#datasetContext) return;\n-\t\tthis.observe(await this.#datasetContext.propertyValueByAlias('blockGroups'), (value) => {\n-\t\t\tthis.#blockGroups = (value as Array<UmbBlockGridTypeGroupType>) ?? [];\n-\t\t\tthis.#mapValuesToBlockGroups();\n-\t\t});\n-\t}\n-\t// TODO: No need for this, we just got the value via the value property.. [NL]\n-\t/*\n-\tasync #observeBlocks() {\n-\t\tif (!this.#datasetContext) return;\n-\t\tthis.observe(await this.#datasetContext.propertyValueByAlias('blocks'), (value) => {\n-\t\t\tthis.value = (value as Array<UmbBlockTypeWithGroupKey>) ?? [];\n-\t\t\tthis.#mapValuesToBlockGroups();\n-\t\t});\n-\t}\n-\t*/\n-\n \t#mapValuesToBlockGroups() {\n@@ -154,36 +141,30 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \n-\t#onDelete(e: CustomEvent, groupKey?: string) {\n-\t\tconst updatedValues = (e.target as UmbInputBlockTypeElement).value.map((value) => ({ ...value, groupKey }));\n-\t\tconst filteredValues = this.#value.filter((value) => value.groupKey !== groupKey);\n-\t\tthis.value = [...filteredValues, ...updatedValues];\n-\t\tthis.dispatchEvent(new UmbPropertyValueChangeEvent());\n-\t}\n-\n-\tasync #onChange(e: CustomEvent) {\n+\tasync #onChange(e: Event, groupKey?: string) {\n \t\te.stopPropagation();\n \t\tconst element = e.target as UmbInputBlockTypeElement;\n-\t\tconst value = element.value;\n-\n-\t\tif (!e.detail?.moveComplete) {\n-\t\t\t// Container change, store data of the new group...\n-\t\t\tconst newGroupKey = element.getAttribute('data-umb-group-key');\n-\t\t\tconst movedItem = e.detail?.item as UmbBlockTypeWithGroupKey;\n-\t\t\t// Check if item moved back to original group...\n-\t\t\tif (movedItem.groupKey === newGroupKey) {\n-\t\t\t\tthis.#moveData = undefined;\n-\t\t\t} else {\n-\t\t\t\tthis.#moveData = value.map((block) => ({ ...block, groupKey: newGroupKey }));\n-\t\t\t}\n-\t\t} else if (e.detail?.moveComplete) {\n-\t\t\t// Move complete, get the blocks that were in an untouched group\n-\t\t\tconst blocks = this.#value\n-\t\t\t\t.filter((block) => !value.find((value) => value.contentElementTypeKey === block.contentElementTypeKey))\n-\t\t\t\t.filter(\n-\t\t\t\t\t(block) => !this.#moveData?.find((value) => value.contentElementTypeKey === block.contentElementTypeKey),\n-\t\t\t\t);\n-\n-\t\t\tthis.value = this.#moveData ? [...blocks, ...value, ...this.#moveData] : [...blocks, ...value];\n-\t\t\tthis.dispatchEvent(new UmbPropertyValueChangeEvent());\n-\t\t\tthis.#moveData = undefined;\n+\t\tconst value = element.value.map((x) => ({ ...x, groupKey }));\n+\n+\t\tif (groupKey) {\n+\t\t\t// Update the specific group:\n+\t\t\tthis._groupsWithBlockTypes = this._groupsWithBlockTypes.map((group) => {\n+\t\t\t\tif (group.key === groupKey) {\n+\t\t\t\t\treturn { ...group, blocks: value };\n+\t\t\t\t}\n+\t\t\t\treturn group;\n+\t\t\t});\n+\t\t} else {\n+\t\t\t// Update the not grouped blocks:\n+\t\t\tthis._notGroupedBlockTypes = value;\n \t\t}\n+\n+\t\tthis.#updateValue();\n+\t}\n+\n+\t#updateValue() {\n+\t\tthis.value = [...this._notGroupedBlockTypes, ...this._groupsWithBlockTypes.flatMap((group) => group.blocks)];\n+\t\tthis.dispatchEvent(new UmbChangeEvent());\n+\t}\n+\n+\t#updateBlockGroupsValue(groups: Array<UmbBlockGridTypeGroupType>) {\n+\t\tthis.#datasetContext?.setPropertyValue('blockGroups', groups);\n \t}\n@@ -193,3 +174,3 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\tif (selectedElementType) {\n-\t\t\tthis.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? null));\n+\t\t\tthis.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? 'null'));\n \t\t}\n@@ -198,15 +179,18 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t// TODO: Implement confirm dialog [NL]\n-\t#deleteGroup(groupKey: string) {\n-\t\t// TODO: make one method for updating the blockGroupsDataSetValue: [NL]\n-\t\t// This one that deletes might require the ability to parse what to send as an argument to the method, then a filtering can occur before.\n-\t\tthis.#datasetContext?.setPropertyValue(\n-\t\t\t'blockGroups',\n-\t\t\tthis.#blockGroups?.filter((group) => group.key !== groupKey),\n-\t\t);\n-\n+\tasync #deleteGroup(groupKey: string) {\n+\t\tconst groupName = this.#blockGroups?.find((group) => group.key === groupKey)?.name ?? '';\n+\t\tawait umbConfirmModal(this, {\n+\t\t\theadline: '#blockEditor_confirmDeleteBlockGroupTitle',\n+\t\t\tcontent: this.localize.term('#blockEditor_confirmDeleteBlockGroupMessage', [groupName]),\n+\t\t\tcolor: 'danger',\n+\t\t\tconfirmLabel: '#general_delete',\n+\t\t});\n \t\t// If a group is deleted, Move the blocks to no group:\n \t\tthis.value = this.#value.map((block) => (block.groupKey === groupKey ? { ...block, groupKey: undefined } : block));\n+\t\tif (this.#blockGroups) {\n+\t\t\tthis.#updateBlockGroupsValue(this.#blockGroups.filter((group) => group.key !== groupKey));\n+\t\t}\n \t}\n \n-\t#changeGroupName(e: UUIInputEvent, groupKey: string) {\n+\t#onGroupNameChange(e: UUIInputEvent, groupKey: string) {\n \t\tconst groupName = e.target.value as string;\n@@ -226,5 +210,4 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t\t\t.workspacePath=${this._workspacePath}\n-\t\t\t\t\t\t@change=${this.#onChange}\n-\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}\n-\t\t\t\t\t\t@delete=${(e: CustomEvent) => this.#onDelete(e, undefined)}></umb-input-block-type>`\n+\t\t\t\t\t\t@change=${(e: Event) => this.#onChange(e, undefined)}\n+\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, undefined)}></umb-input-block-type>`\n \t\t\t\t: ''}\n@@ -241,5 +224,4 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t\t\t\t.workspacePath=${this._workspacePath}\n-\t\t\t\t\t\t\t@change=${this.#onChange}\n-\t\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}\n-\t\t\t\t\t\t\t@delete=${(e: CustomEvent) => this.#onDelete(e, group.key)}></umb-input-block-type>\n+\t\t\t\t\t\t\t@change=${(e: Event) => this.#onChange(e, group.key)}\n+\t\t\t\t\t\t\t@create=${(e: CustomEvent) => this.#onCreate(e, group.key)}></umb-input-block-type>\n \t\t\t\t\t</div>`,\n@@ -255,3 +237,3 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement\n \t\t\t\t.value=${groupName ?? ''}\n-\t\t\t\t@change=${(e: UUIInputEvent) => this.#changeGroupName(e, groupKey)}>\n+\t\t\t\t@change=${(e: UUIInputEvent) => this.#onGroupNameChange(e, groupKey)}>\n \t\t\t\t<uui-button compact slot=\"append\" label=\"delete\" @click=${() => this.#deleteGroup(groupKey)}>\n","improvement-type":"Complex Method"}],"change-level":"warning","is-hotspot?":false,"line":76,"what-changed":"UmbElementTableCollectionViewElement.createTableItems has a cyclomatic complexity of 9, threshold = 9","how-to-fix":"There are many reasons for Complex Method. Sometimes, another design approach is beneficial such as a) modeling state using an explicit state machine rather than conditionals, or b) using table lookup rather than long chains of logic. In other scenarios, the function can be split using [EXTRACT FUNCTION](https://refactoring.com/catalog/extractFunction.html). Just make sure you extract natural and cohesive functions. Complex Methods can also be addressed by identifying complex conditional expressions and then using the [DECOMPOSE CONDITIONAL](https://refactoring.com/catalog/decomposeConditional.html) refactoring.","change-type":"introduced"}]},"positive-impact-count":1,"repo":"Umbraco-CMS","code-health":9.318636940447997,"version":"3.0","authors":["leekelleher","Lee Kelleher","Jacob Overgaard"],"directives":{"added":[],"removed":[]},"positive-findings":{"number-of-types":1,"number-of-files-touched":1,"findings":[{"name":"Overall Code Complexity","file":"src/Umbraco.Web.UI.Client/src/packages/elements/publishing/workspace-context/element-publishing.workspace-context.ts","change-type":"improved","change-level":"improvement","is-hotspot?":false,"why-it-occurs":"Overall Code Complexity is measured by the mean cyclomatic complexity across all functions in the file. The lower the number, the better.\n\nCyclomatic complexity is a function level metric that measures the number of logical branches (if-else, loops, etc.). Cyclomatic complexity is a rough complexity measure, but useful as a way of estimating the minimum number of unit tests you would need. As such, prefer functions with low cyclomatic complexity (2-3 branches).","how-to-fix":"You address the overall cyclomatic complexity by a) modularizing the code, and b) abstract away the complexity. Let's look at some examples:\n\nModularizing the Code: Do an X-Ray and inspect the local hotspots. Are there any complex conditional expressions? If yes, then do a [DECOMPOSE CONDITIONAL](https://refactoring.com/catalog/decomposeConditional.html) refactoring. Extract the conditional logic into a separate function and put a good name on that function. This clarifies the intent and makes the original function easier to read. Repeat until all complex conditional expressions have been simplified.\n\n","what-changed":"The mean cyclomatic complexity decreases from 6.77 to 6.11, threshold = 4"}]},"notices":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"external-review-provider":"GitHub"},"analysistime":"2026-04-07T08:21:27.000Z","project-name":"Umbraco-CMS","repository":"https://github.com/umbraco/Umbraco-CMS.git"}}