fix(ArrowAnnotate): Allow free-form text for arrow annotations (#5259)

This commit is contained in:
Joe Boccanfuso 2025-07-24 12:58:45 -04:00 committed by GitHub
parent 4e6530d1d8
commit 3c8446a5e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 156 additions and 11 deletions

View File

@ -637,10 +637,22 @@ function commandsModule({
viewportGridService.setActiveViewportId(viewportId);
},
arrowTextCallback: async ({ callback }) => {
arrowTextCallback: async ({ callback, data }) => {
const labelConfig = customizationService.getCustomization('measurementLabels');
const renderContent = customizationService.getCustomization('ui.labellingComponent');
if (!labelConfig) {
const label = await callInputDialog({
uiDialogService,
title: 'Edit Arrow Text',
placeholder: data?.data?.label || 'Enter new text',
defaultValue: data?.data?.label || '',
});
callback?.(label);
return;
}
const value = await callInputDialogAutoComplete({
uiDialogService,
labelConfig,

View File

@ -409,10 +409,6 @@ const connectMeasurementServiceToTools = ({
setAnnotationLabel(sourceAnnotation, element, label);
}
if (metadata.toolName === 'ArrowAnnotate') {
data.text = label;
}
// update the isLocked state
annotation.locking.setAnnotationLocked(uid, isLocked);

View File

@ -75,7 +75,7 @@ const ArrowAnnotate = {
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
toolName: metadata.toolName,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: data.text,
label: data.label,
displayText: displayText,
data: data.cachedStats,
type: getValueTypeFromToolType(toolName),

View File

@ -295,6 +295,7 @@ export const DataRow: React.FC<DataRowProps> = ({
: 'opacity-0 group-hover:opacity-100'
}`}
aria-label="Actions"
dataCY="actionsMenuTrigger"
onClick={e => e.stopPropagation()} // Prevent row selection on button click
>
<Icons.More className="h-6 w-6" />
@ -308,21 +309,41 @@ export const DataRow: React.FC<DataRowProps> = ({
<>
<DropdownMenuItem onClick={e => handleAction('Rename', e)}>
<Icons.Rename className="text-foreground" />
<span className="pl-2">Rename</span>
<span
className="pl-2"
data-cy="Rename"
>
Rename
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={e => handleAction('Delete', e)}>
<Icons.Delete className="text-foreground" />
<span className="pl-2">Delete</span>
<span
className="pl-2"
data-cy="Delete"
>
Delete
</span>
</DropdownMenuItem>
{onColor && (
<DropdownMenuItem onClick={e => handleAction('Color', e)}>
<Icons.ColorChange className="text-foreground" />
<span className="pl-2">Change Color</span>
<span
className="pl-2"
data-cy="Change Color"
>
Change Color
</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={e => handleAction('Lock', e)}>
<Icons.Lock className="text-foreground" />
<span className="pl-2">{isLocked ? 'Unlock' : 'Lock'}</span>
<span
className="pl-2"
data-cy="LockToggle"
>
{isLocked ? 'Unlock' : 'Lock'}
</span>
</DropdownMenuItem>
</>
</DropdownMenuContent>

View File

@ -125,6 +125,7 @@ const InputDialogInput = React.forwardRef<HTMLInputElement, InputDialogInputProp
<InputComponent
ref={inputRef}
id={id}
data-cy={id}
value={value}
onChange={e => setValue(e.target.value)}
onKeyDown={handleKeyDown}

View File

@ -24,6 +24,8 @@ test.describe('3D four up Test', async () => {
await attemptAction(() => reduce3DViewportSize(page), 10, 100);
await page.waitForTimeout(5000);
await checkForScreenshot(
page,
page,

View File

@ -22,6 +22,7 @@ test.describe('3D main Test', async () => {
.first()
.click();
await attemptAction(() => reduce3DViewportSize(page), 10, 100);
await page.waitForTimeout(5000);
await checkForScreenshot(page, page, screenShotPaths.threeDMain.threeDMainDisplayedCorrectly);
});
});

View File

@ -22,6 +22,7 @@ test.describe('3D only Test', async () => {
.first()
.click();
await attemptAction(() => reduce3DViewportSize(page), 10, 100);
await page.waitForTimeout(5000);
// Use a 4 percent diff pixel ratio to account for slight color differences in the 3D viewport
await checkForScreenshot({
page,

View File

@ -23,6 +23,7 @@ test.describe('3D primary Test', async () => {
.click();
await attemptAction(() => reduce3DViewportSize(page), 10, 100);
await page.waitForTimeout(5000);
await checkForScreenshot(
page,
page,

View File

@ -0,0 +1,86 @@
import { test } from 'playwright-test-coverage';
import {
visitStudy,
checkForScreenshot,
screenShotPaths,
simulateClicksOnElement,
simulateDoubleClickOnElement,
} from './utils';
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test('should display the arrow tool and allow free-form text to be entered', async ({ page }) => {
await page.getByTestId('trackedMeasurements-btn').click();
await page.getByTestId('MeasurementTools-split-button-secondary').click();
await page.getByTestId('ArrowAnnotate').click();
const locator = page.getByTestId('viewport-pane').locator('canvas');
await simulateClicksOnElement({
locator,
points: [
{
x: 164,
y: 234,
},
{
x: 344,
y: 232,
},
],
});
await page.getByTestId('dialog-input').fill('Ringo Starr was the drummer for The Beatles');
await page.getByTestId('input-dialog-save-button').click();
await page.getByTestId('prompt-begin-tracking-yes-btn').click();
await page.waitForTimeout(2000);
await checkForScreenshot({
page,
maxDiffPixelRatio: 0.0075,
screenshotPath: screenShotPaths.arrowAnnotate.arrowAnnotateDisplayedCorrectly0,
});
// Now edit the arrow text and the label should not change.
await simulateDoubleClickOnElement({
locator,
point: {
x: 164,
y: 234,
},
});
await page.getByTestId('dialog-input').fill('Neil Peart was the drummer for Rush');
await page.getByTestId('input-dialog-save-button').click();
await page.waitForTimeout(2000);
await checkForScreenshot({
page,
maxDiffPixelRatio: 0.0075,
screenshotPath: screenShotPaths.arrowAnnotate.arrowAnnotateDisplayedCorrectly1,
});
// Now edit the label and the text should not change.
await page.getByTestId('actionsMenuTrigger').click();
await page.getByTestId('Rename').click();
await page.getByTestId('dialog-input').fill('Drummer annotation arrow');
await page.getByTestId('input-dialog-save-button').click();
await page.waitForTimeout(2000);
await checkForScreenshot({
page,
maxDiffPixelRatio: 0.0075,
screenshotPath: screenShotPaths.arrowAnnotate.arrowAnnotateDisplayedCorrectly2,
});
});

View File

@ -14,12 +14,16 @@ test('should render scroll bars with the correct look-and-feel', async ({ page }
);
await studyRowHeader.scrollIntoViewIfNeeded();
await page.waitForTimeout(3000);
await studyRowHeader.click();
const expandedStudyRow = await page.getByTestId(
'studyRow-1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868'
);
await expandedStudyRow.scrollIntoViewIfNeeded();
await page.waitForTimeout(3000);
await checkForScreenshot({
page,

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

View File

@ -3,6 +3,7 @@ import { checkForScreenshot } from './checkForScreenshot';
import { screenShotPaths } from './screenShotPaths';
import {
simulateClicksOnElement,
simulateDoubleClickOnElement,
simulateNormalizedClickOnElement,
simulateNormalizedClicksOnElement,
} from './simulateClicksOnElement';
@ -20,6 +21,7 @@ export {
checkForScreenshot,
screenShotPaths,
simulateClicksOnElement,
simulateDoubleClickOnElement,
simulateNormalizedClickOnElement,
simulateNormalizedClicksOnElement,
reduce3DViewportSize,

View File

@ -2,6 +2,11 @@
* Paths to the screenshots of the tests.
*/
const screenShotPaths = {
arrowAnnotate: {
arrowAnnotateDisplayedCorrectly0: 'arrowAnnotateDisplayedCorrectly0.png',
arrowAnnotateDisplayedCorrectly1: 'arrowAnnotateDisplayedCorrectly1.png',
arrowAnnotateDisplayedCorrectly2: 'arrowAnnotateDisplayedCorrectly2.png',
},
angle: {
angleDisplayedCorrectly: 'angleDisplayedCorrectly.png',
},

View File

@ -9,15 +9,28 @@ import { Locator } from 'playwright';
export async function simulateClicksOnElement({
locator,
points,
button = 'left',
}: {
locator: Locator;
points: { x: number; y: number }[];
button?: 'left' | 'right' | 'middle';
}) {
for (const { x, y } of points) {
await locator.click({ delay: 100, position: { x, y } });
await locator.click({ delay: 100, position: { x, y }, button });
}
}
export async function simulateDoubleClickOnElement({
locator,
point,
}: {
locator: Locator;
point: { x: number; y: number };
}) {
const { x, y } = point;
await locator.dblclick({ delay: 100, position: { x, y } });
}
/**
* Simulates clicks on an element at a normalized point.
*