Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui) Migrate Labelflow component to ui-next (Early WIP) #4639

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 70 additions & 131 deletions extensions/default/src/utils/callInputDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,158 +1,97 @@
import React from 'react';
import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui';
// EARLY DRAFT WITH NEW COMPONENTS

/**
*
* @param {*} data
* @param {*} data.text
* @param {*} data.label
* @param {*} event
* @param {*} callback
* @param {*} isArrowAnnotateInputDialog
* @param {*} dialogConfig
* @param {string?} dialogConfig.dialogTitle - title of the input dialog
* @param {string?} dialogConfig.inputLabel - show label above the input
*/
import React, { useState } from 'react';
// We'll import Popover + other essentials from ui-next
import { Popover, PopoverTrigger, PopoverContent, Button, Input } from '@ohif/ui-next';

export function callInputDialog(
uiDialogService,
data,
callback,
isArrowAnnotateInputDialog = true,
dialogConfig: any = {}
dialogConfig = {}
) {
const dialogId = 'dialog-enter-annotation';
const dialogId = 'popover-label-annotation';
const label = data ? (isArrowAnnotateInputDialog ? data.text : data.label) : '';
const {
dialogTitle = 'Annotation',
inputLabel = 'Enter your annotation',
validateFunc = value => true,
validateFunc = val => true,
} = dialogConfig;

const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
if (typeof validateFunc === 'function' && !validateFunc(value.label)) {
return;
}
// Instead of creating a modal, we create a component that uses Popover
// and pass it to uiDialogService (or render it directly in your UI).
const PopoverLabelPrompt = () => {
const [open, setOpen] = useState(true);
const [value, setValue] = useState(label);

callback(value.label, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
const onCancel = () => {
callback('', 'cancel');
setOpen(false);
uiDialogService.dismiss({ id: dialogId });
};

if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: dialogTitle,
value: { label },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Save', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
return (
const onSave = () => {
if (!validateFunc(value)) {
return;
}
callback(value, 'save');
setOpen(false);
uiDialogService.dismiss({ id: dialogId });
};

return (
<Popover
open={open}
onOpenChange={setOpen}
>
{/* A Popover usually needs a Trigger. You can hide it with a hidden button or anchor */}
<PopoverTrigger asChild>
<Button className="hidden" />
</PopoverTrigger>
<PopoverContent className="flex w-72 flex-col gap-2">
<div className="text-primary-light text-lg font-semibold">{dialogTitle}</div>
<div className="flex flex-col gap-2">
<label className="text-sm">{inputLabel}</label>
<Input
autoFocus
className="border-primary-main bg-black"
type="text"
id="annotation"
label={inputLabel}
labelClassName="text-white text-[14px] leading-[1.2]"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
value={value}
onChange={e => setValue(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
onSave();
}
}}
/>
);
},
},
});
}
}

export function callLabelAutocompleteDialog(uiDialogService, callback, dialogConfig, labelConfig) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];

const { validateFunc = value => true } = dialogConfig;

const labellingDoneCallback = value => {
if (typeof value === 'string') {
if (typeof validateFunc === 'function' && !validateFunc(value)) {
return;
}
callback(value, 'save');
} else {
callback('', 'cancel');
}
uiDialogService.dismiss({ id: 'select-annotation' });
</div>
<div className="mt-2 flex justify-end gap-2">
<Button
variant="outline"
onClick={onCancel}
>
Cancel
</Button>
<Button
variant="default"
onClick={onSave}
>
Save
</Button>
</div>
</PopoverContent>
</Popover>
);
};

uiDialogService.create({
id: 'select-annotation',
centralize: true,
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: { label: '' },
componentClassName: {},
labelData: dropDownItems,
exclusive: exclusive,
},
});
}

export function showLabelAnnotationPopup(measurement, uiDialogService, labelConfig) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
return new Promise<Map<any, any>>((resolve, reject) => {
const labellingDoneCallback = value => {
uiDialogService.dismiss({ id: 'select-annotation' });
if (typeof value === 'string') {
measurement.label = value;
}
resolve(measurement);
};

// Now we create that popover component with uiDialogService
if (uiDialogService) {
uiDialogService.create({
id: 'select-annotation',
id: dialogId,
centralize: false,
isDraggable: false,
showOverlay: true,
content: LabellingFlow,
defaultPosition: {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
},
contentProps: {
labellingDoneCallback: labellingDoneCallback,
measurementData: measurement,
componentClassName: {},
labelData: dropDownItems,
exclusive: exclusive,
},
showOverlay: false, // We might not need an overlay for a popover
content: PopoverLabelPrompt,
contentProps: {},
});
});
}
}

export default callInputDialog;
41 changes: 41 additions & 0 deletions platform/ui-next/src/components/OHIFLabel/InputRadio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { Component } from 'react';
import { LabelInfo } from './types';

interface InputRadioProps {
value: string;
label: string;
itemData: LabelInfo;
id: string;
onSelected: Function;
index: number;
selectTree: any; // reference to parent SelectTree component
}

export default class InputRadio extends Component<InputRadioProps> {
render() {
const { focusedIndex } = this.props.selectTree.state;
const isFocused = this.props.index === focusedIndex;

return (
<label
className={`block h-10 w-full cursor-pointer overflow-hidden border-b border-b-gray-900 pl-3 leading-10 ${
isFocused ? 'bg-black' : ''
}`}
htmlFor={this.props.id}
>
<input
type="radio"
id={this.props.id}
className="hidden"
value={this.props.value}
onChange={this.onSelected}
/>
<span className="font-labels">{this.props.label}</span>
</label>
);
}

onSelected = (evt: React.ChangeEvent<HTMLInputElement>) => {
this.props.onSelected(evt, this.props.itemData);
};
}
33 changes: 33 additions & 0 deletions platform/ui-next/src/components/OHIFLabel/LabellingTransition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { Component } from 'react';
import { CSSTransition } from 'react-transition-group';

const transitionDuration = 0;
const transitionClassName = 'labelling';
const transitionOnAppear = true;

interface LabellingTransitionProps {
children: React.ReactNode;
displayComponent: boolean;
onTransitionExit: () => void;
}

/**
* A simple transition wrapper used to animate/hide the labelling flow
*/
export default class LabellingTransition extends Component<LabellingTransitionProps> {
render() {
const { displayComponent, onTransitionExit, children } = this.props;

return (
<CSSTransition
in={displayComponent}
appear={transitionOnAppear}
timeout={transitionDuration}
classNames={transitionClassName}
onExited={onTransitionExit}
>
{children}
</CSSTransition>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { Component, createRef } from 'react';
import cloneDeep from 'lodash.clonedeep';
import { LabelInfo } from './types';
import LabellingTransition from './LabellingTransition';
import SelectTree from './SelectTree';

interface OHIFLabellingFlowProps {
labellingDoneCallback: (label: string) => void;
measurementData: any;
labelData: any;
exclusive: boolean;
componentClassName: any;
}

interface OHIFLabellingFlowState {
label: string;
componentClassName: any;
confirmationState: boolean;
displayComponent: boolean;
}

class OHIFLabellingFlow extends Component<OHIFLabellingFlowProps, OHIFLabellingFlowState> {
currentItems: Array<LabelInfo> = [];
mainElement: React.RefObject<HTMLDivElement>;

constructor(props: OHIFLabellingFlowProps) {
super(props);
const { label } = props.measurementData;
const className = props.componentClassName || {};

this.state = {
label,
componentClassName: className,
confirmationState: false,
displayComponent: true,
};

this.mainElement = createRef();
}

render() {
const { labelData } = this.props;
if (labelData) {
this.currentItems = cloneDeep(labelData);
}

return (
<LabellingTransition
displayComponent={this.state.displayComponent}
onTransitionExit={this.props.labellingDoneCallback}
>
<div
className={this.state.componentClassName}
ref={this.mainElement}
>
{this.labellingStateFragment()}
</div>
</LabellingTransition>
);
}

closePopup = () => {
this.setState({ displayComponent: false });

// Optionally a delayed set if you want some effect:
setTimeout(() => {
this.setState({ displayComponent: false });
}, 2000);
};

selectTreeSelectCallback = (_event: any, itemSelected: LabelInfo) => {
const label = itemSelected.value;
this.closePopup();
return this.props.labellingDoneCallback(label);
};

labellingStateFragment = () => {
return (
<SelectTree
items={this.currentItems}
columns={1}
onSelected={this.selectTreeSelectCallback}
closePopup={this.closePopup}
selectTreeFirstTitle="Annotation"
exclusive={this.props.exclusive}
label={this.state.label}
/>
);
};
}

export default OHIFLabellingFlow;
Loading
Loading