Skip to content
18 changes: 9 additions & 9 deletions src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ class UmbLogViewerMessagesData extends UmbMockDBBase<LogMessageResponseModel> {
return this.data.slice(skip, take);
}

getLevelCount() {
const levels = this.data.map((log) => log.level?.toLowerCase() ?? 'unknown');
const counts = {};
levels.forEach((level: string) => {
//eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
counts[level ?? 'unknown'] = (counts[level] || 0) + 1;
});
return counts;
getLevelCount(): Record<string, number> {
return this.data.reduce(
(counts, log) => {
const level = log.level ?? 'unknown';
counts[level] = (counts[level] || 0) + 1;
return counts;
},
{} as Record<string, number>,
);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Hook up a click listener to the window that, for all anchor tags
* Hook up a click listener to the window that, for all anchor tags (HTML or SVG)
* that has a relative HREF, uses the history API instead.
*/
export function ensureAnchorHistory() {
Expand All @@ -10,37 +10,50 @@
if ((isWindows && e.ctrlKey) || (!isWindows && e.metaKey)) return;

// Find the target by using the composed path to get the element through the shadow boundaries.
// Support both HTML anchor tags and SVG anchor tags
const $anchor = (('composedPath' in e) as any)
? e.composedPath().find(($elem) => $elem instanceof HTMLAnchorElement)
? e.composedPath().find(($elem) => $elem instanceof HTMLAnchorElement || $elem instanceof SVGAElement)
: e.target;

// Abort if the event is not about the anchor tag
if ($anchor == null || !($anchor instanceof HTMLAnchorElement)) {
// Abort if the event is not about an anchor tag (HTML or SVG)
if ($anchor == null || !($anchor instanceof HTMLAnchorElement || $anchor instanceof SVGAElement)) {
return;
}

// Get the HREF value from the anchor tag
const href = $anchor.href;
// SVGAElement.href returns SVGAnimatedString, so we need to access .baseVal
const href = $anchor instanceof SVGAElement ? $anchor.href.baseVal : $anchor.href;
const target = $anchor instanceof SVGAElement ? $anchor.target.baseVal : $anchor.target;

// For SVG anchors, we need to construct a full URL to extract pathname, search, and hash
// For HTML anchors, these properties are directly available
let fullUrl: URL;
try {
// Use the current document location as the base to resolve relative URLs
// This respects the <base> tag and works the same as HTML anchors
// Note: This may resolve into an external URL, but we validate that later
fullUrl = new URL(href, document.location.origin);
} catch {
// Invalid URL, skip
return;
}

// Only handle the anchor tag if the follow holds true:
// - The HREF is relative to the origin of the current location.
// - The target is targeting the current frame.
// - The anchor doesn't have the attribute [data-router-slot]="disabled"
if (
!href.startsWith(location.origin) ||
($anchor.target !== '' && $anchor.target !== '_self') ||
fullUrl.origin !== location.origin ||
(target !== '' && target !== '_self') ||

Check warning on line 47 in src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/util/anchor.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Complex Conditional

ensureAnchorHistory.'click' increases from 2 complex conditionals with 6 branches to 3 complex conditionals with 8 branches, threshold = 2. A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple, logical operators such as AND/OR. The more logical operators in an expression, the more severe the code smell.
$anchor.dataset['routerSlot'] === 'disabled'
) {
return;
}

// Remove the origin from the start of the HREF to get the path
const path = $anchor.pathname + $anchor.search + $anchor.hash;

// Prevent the default behavior
e.preventDefault();

// Change the history!
history.pushState(null, '', path);
history.pushState(null, '', fullUrl);
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The history.pushState API expects a string for the URL parameter, not a URL object. While the URL object will be coerced to a string, it will include the full absolute URL (e.g., https://example.com/path) rather than just the path portion.

This differs from the previous implementation which passed pathname + search + hash, and may cause issues with the history state.

Recommend changing to:

history.pushState(null, '', fullUrl.pathname + fullUrl.search + fullUrl.hash);
Suggested change
history.pushState(null, '', fullUrl);
history.pushState(null, '', fullUrl.pathname + fullUrl.search + fullUrl.hash);

Copilot uses AI. Check for mistakes.
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface Circle {
percent: number;
kind: string;
number: number;
href: string;
}

interface CircleWithCommands extends Circle {
Expand Down Expand Up @@ -72,6 +73,20 @@ export class UmbDonutChartElement extends LitElement {
@property({ type: Boolean })
hideDetailBox = false;

/**
* Shows numbers inside each slice of the donut chart
* @memberof UmbDonutChartElement
*/
@property({ type: Boolean, attribute: 'show-inline-numbers' })
showInlineNumbers = false;

/**
* Shows the description text below the chart
* @memberof UmbDonutChartElement
*/
@property({ type: Boolean, attribute: 'show-description' })
showDescription = false;

@queryAssignedElements({ selector: 'umb-donut-slice' })
private _slices!: UmbDonutSliceElement[];

Expand Down Expand Up @@ -141,6 +156,7 @@ export class UmbDonutChartElement extends LitElement {
color: slice.color,
name: slice.name,
kind: slice.kind,
href: slice.href,
};
}),
);
Expand Down Expand Up @@ -180,11 +196,28 @@ export class UmbDonutChartElement extends LitElement {
return [coordX, coordY].join(' ');
}

#getTextPosition(circle: CircleWithCommands): { x: number; y: number } {
// Calculate the middle angle of the slice
const startAngle = -circle.offset;
const sliceDegrees = UmbDonutChartElement.percentToDegrees(circle.percent);
const middleAngle = startAngle + sliceDegrees / 2;

// Position the text at the middle of the donut ring
const textRadius = this.radius - this.borderSize / 2;
const angleRad = (middleAngle * Math.PI) / 180;
const x = Math.cos(angleRad) * textRadius + this.svgSize / 2;
const y = -Math.sin(angleRad) * textRadius + this.svgSize / 2;

return { x, y };
}

#calculateDetailsBoxPosition = (event: MouseEvent) => {
// Recalculate bounds on each mouse move to handle window resize
this.#containerBounds = this._container.getBoundingClientRect();
const x = this.#containerBounds ? event.clientX - this.#containerBounds?.left : 0;
const y = this.#containerBounds ? event.clientY - this.#containerBounds?.top : 0;
this._posX = x - 10;
this._posY = y - 70;
this._posX = x + 10;
this._posY = y + 10;
};
Comment on lines 214 to 221
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The #calculateDetailsBoxPosition method is called on every mousemove event and recalculates getBoundingClientRect() each time. This could impact performance, especially with many rapid mouse movements.

Consider throttling or debouncing the position calculation, or only recalculating the bounds when the window resizes rather than on every mouse move. The bounds don't change during mouse movement unless the window is resized.

Copilot uses AI. Check for mistakes.

#setDetailsBoxData(event: MouseEvent) {
Expand Down Expand Up @@ -231,11 +264,10 @@ export class UmbDonutChartElement extends LitElement {
<feDropShadow stdDeviation="1 1" in="merge1" dx="0" dy="0" flood-color="#000" flood-opacity="0.8" x="0%" y="0%" width="100%" height="100%" result="dropShadow1"/>
</filter>
<desc>${this.description}</desc>
${this._circles.map(
(circle, i) => svg`
${this._circles.map((circle, i) => {
const content = svg`
<path
class="circle"

data-index="${i}"
fill="${circle.color}"
role="listitem"
Expand All @@ -251,22 +283,41 @@ export class UmbDonutChartElement extends LitElement {
role="listitem"
d="${circle.commands}"
transform="rotate(${circle.offset} ${this._viewBox / 2} ${this._viewBox / 2})">
</path>`,
)}
</path>
${
this.showInlineNumbers
? svg`<text
class="slice-number"
x="${this.#getTextPosition(circle).x}"
y="${this.#getTextPosition(circle).y}"
text-anchor="middle"
dominant-baseline="middle"
fill="white"
font-weight="bold"
font-size="${this.borderSize * 0.6}px"
pointer-events="none">${circle.number}</text>`
: ''
}`;

return circle.href ? svg`<a href="${circle.href}">${content}</a>` : content;
})}

`;
}

override render() {
return html` <div id="container" @mousemove=${this.#calculateDetailsBoxPosition}>
<svg viewBox="0 0 ${this._viewBox} ${this._viewBox}" role="list">${this.#renderCircles()}</svg>
<svg width="100%" height="100%" viewBox="0 0 ${this._viewBox} ${this._viewBox}" role="list">
${this.#renderCircles()}
</svg>
<div
id="details-box"
style="--pos-y: ${this._posY}px; --pos-x: ${this._posX}px; --umb-donut-detail-color: ${this._detailColor}">
<div id="details-title"><uui-icon name="icon-record"></uui-icon>${this._detailName}</div>
<span>${this._detailAmount} ${this._detailKind}</span>
</div>
</div>
${this.showDescription && this.description ? html`<p class="description">${this.description}</p>` : ''}
<slot @slotchange=${this.#printCircles} @slice-update=${this.#printCircles}></slot>`;
}

Expand All @@ -292,7 +343,9 @@ export class UmbDonutChartElement extends LitElement {

#container {
position: relative;
width: 200px;
width: 100%;
max-width: 200px;
aspect-ratio: 1;
}

#details-box {
Expand All @@ -311,6 +364,7 @@ export class UmbDonutChartElement extends LitElement {
transform: translate3d(var(--pos-x), var(--pos-y), 0);
transition: transform 0.2s cubic-bezier(0.02, 1.23, 0.79, 1.08);
transition: opacity 150ms linear;
pointer-events: none;
}

#details-box.show {
Expand All @@ -328,6 +382,17 @@ export class UmbDonutChartElement extends LitElement {
display: flex;
align-items: center;
}

.slice-number {
user-select: none;
}

.description {
text-align: center;
font-size: var(--uui-type-small-size);
color: var(--uui-color-text-alt);
margin: var(--uui-size-space-2) 0 0 0;
}
`,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export class UmbDonutSliceElement extends LitElement {
@property()
kind = '';

/**
* Optional href to make the slice clickable
* @memberof UmbDonutSliceElement
*/
@property()
href = '';

override willUpdate() {
this.dispatchEvent(new CustomEvent('slice-update', { composed: true, bubbles: true }));
}
Expand Down
Loading
Loading