{"results":{"result":{"added-files":{"code-health":0.0,"old-code-health":0.0,"files":[]},"external-review-url":"https://github.com/umbraco/Umbraco-CMS/pull/20157","old-code-health":9.048270622436526,"modified-files":{"code-health":8.997354731618337,"old-code-health":9.048270622436526,"files":[{"file":"src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts","loc":279,"old-loc":274,"code-health":8.997354731618337,"old-code-health":9.048270622436526}]},"removed-files":{"code-health":0.0,"old-code-health":0.0,"files":[]},"external-review-id":"20157","analysis-time":"2025-09-17T10:33:54Z","negative-impact-count":1,"suppressions":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"affected-hotspots":1,"commits":["84dc07cbdb9ce0f25fe2a6c7ef410bfaff3b72f4","55d905ea14bababe6a49faa4b2fc8dbe179a156a","5360df95bbd1f79b65aa5e40e68b3b835c02ef57","e71693cac3336260f273e508489451c6f81f875f","afb468c1cc12d8be196e233e2d6e64b1a27eadda","6d9018ce42d530bc244eab274e9085ebc36e6850","caaf57ee719a5ac37f60b3d1eaa6088df148343b","889b8ed689e2cfb7c0c82db193efd02993e35cfe","443c5cac338fc614d792472e5d354c803c120a3a"],"is-negative-review":true,"negative-findings":{"number-of-types":1,"number-of-files-touched":1,"findings":[{"method":"UmbInputTiptapElement.loadEditor","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/tiptap/components/input-tiptap/input-tiptap.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?":true,"line":141,"what-changed":"UmbInputTiptapElement.loadEditor increases in cyclomatic complexity from 13 to 15, 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":"degraded"}]},"positive-impact-count":0,"repo":"Umbraco-CMS","code-health":8.997354731618337,"version":"3.0","authors":["Oskar kruger","leekelleher"],"directives":{"added":[],"removed":[]},"positive-findings":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"notices":{"number-of-types":0,"number-of-files-touched":0,"findings":[]},"external-review-provider":"GitHub"},"analysistime":"2025-09-17T10:33:54.000Z","project-name":"Umbraco-CMS","repository":"https://github.com/umbraco/Umbraco-CMS.git"}}