web/src/components/task/task-sidebar/task-sidebar.tsx (639 lines of code) (raw):

// temporary_disabled_rules /* eslint-disable react-hooks/exhaustive-deps */ import React, { FC, ReactElement, useEffect, useMemo, useState } from 'react'; import { Button, FlexRow, LabeledInput, MultiSwitch, NumericInput, Panel, RadioGroup, TabButton, Tooltip, TextInput } from '@epam/loveship'; import { useGetPageSummary } from 'api/hooks/tasks'; import { useTaskAnnotatorContext } from 'connectors/task-annotator-connector/task-annotator-context'; import { CategoriesSelectionModeToggle } from 'components/categories/categories-selection-mode-toggle/categories-selection-mode-toggle'; import { useTableAnnotatorContext } from '../../../shared/components/annotator/context/table-annotator-context'; import { TaskSidebarData } from '../task-sidebar-data/task-sidebar-data'; import { Annotation, AnnotationBoundMode, AnnotationBoundType, AnnotationImageToolType, AnnotationLinksBoundType } from 'shared'; import { Status } from 'shared/components/status'; import { mapStatusForValidationPage } from 'shared/helpers/map-statuses'; import { ValidationPageStatus } from 'api/typings/tasks'; import { Label } from '../../../api/typings'; import { ImageToolsParams } from './image-tools-params'; import { CategoriesTab } from 'components/categories/categories-tab/categories-tab'; import { useLinkTaxonomyByCategoryAndJobId } from 'api/hooks/taxons'; import { TaskSidebarLabelsLinks } from './task-sidebar-labels-links/task-sidebar-labels-links'; import { NoData } from 'shared/no-data'; import { ReactComponent as MergeIcon } from '@epam/assets/icons/common/editor-table_merge_cells-24.svg'; import { ReactComponent as SplitIcon } from '@epam/assets/icons/common/editor-table_split_cells-24.svg'; import { getCategoryDataAttrs } from 'connectors/task-annotator-connector/task-annotator-utils'; import { FinishButton } from './finish-button/finish-button'; import { TABS, VISIBILITY_SETTING_ID } from './constants'; import { ReactComponent as openIcon } from '@epam/assets/icons/common/navigation-chevron-left_left-18.svg'; import { ReactComponent as closeIcon } from '@epam/assets/icons/common/navigation-chevron-right_right-18.svg'; import { ReactComponent as Copy } from '@epam/assets/icons/common/copy_content-12.svg'; import { handleCopy } from 'shared/helpers/copy-text'; import { getSaveButtonTooltipContent } from './utils'; import styles from './task-sidebar.module.scss'; type TaskSidebarProps = { jobSettings?: ReactElement; viewMode: boolean; isNextTaskPresented?: boolean; }; const TaskSidebar: FC<TaskSidebarProps> = ({ jobSettings, viewMode, isNextTaskPresented }) => { const [isHidden, setIsHidden] = useState<boolean>(() => { const savedValue = localStorage.getItem(VISIBILITY_SETTING_ID); return savedValue ? JSON.parse(savedValue) : false; }); const [tableModeValues, setTableModeValues] = useState<string>(''); const [boundModeSwitch, setBoundModeSwitch] = useState<AnnotationBoundMode>('box'); const { annDataAttrs, task, job, categories, fileMetaInfo, currentPage, validPages, invalidPages, selectedAnnotation, editedPages, touchedPages, modifiedPages, tabValue, selectionType, annotationType, isCategoryDataEmpty, onValidClick, onInvalidClick, onSaveTask, onAnnotationTaskFinish, onEditClick, onClearTouchedPages, onClearModifiedPages, clearAnnotationsChanges, onAddTouchedPage, onCancelClick, onSaveEditClick, setTabValue, onChangeSelectionType, onDataAttributesChange, onAnnotationEdited, tableMode, tableCellCategory, setTableCellCategory, selectedTool, onChangeSelectedTool, selectedToolParams, setSelectedToolParams, onLabelsSelected, setSelectedLabels, selectedLabels, latestLabelsId, isDocLabelsModified, getJobId, documentLinks, onRelatedDocClick, selectedRelatedDoc, documentLinksChanged, onFinishSplitValidation, allValidated, annotationSaved, onFinishValidation, notProcessedPages, currentCell, allAnnotations, setTableCellsModified } = useTaskAnnotatorContext(); const { tableModeColumns, tableModeRows, setTableModeRows, setTableModeColumns, setIsCellMode, cellsSelected, onMergeCellsClicked, selectedCellsCanBeMerged, onSplitCellsClicked, selectedCellsCanBeSplitted } = useTableAnnotatorContext(); const isValidation = task?.is_validation; const isAnnotatable = task?.status === 'In Progress' || task?.status === 'Ready'; const isValid = validPages.includes(currentPage); const isInvalid = invalidPages.includes(currentPage); const editPage = editedPages.includes(currentPage); const splitValidation = isValidation && job?.validation_type === 'extensive_coverage'; const isValidationDisabled = (!currentPage && !splitValidation) || !isAnnotatable; const { refetch } = useGetPageSummary({ taskId: task?.id, taskType: task?.is_validation }, {}); const [cell, setCell] = useState<string | undefined>(''); const [annotation, setAnnotation] = useState<Annotation>(); useEffect(() => { setCell(currentCell?.text); }, [currentCell]); useEffect(() => { if (allAnnotations) { const annotation = allAnnotations[currentPage]; if (annotation) { setAnnotation(annotation[0]); } } }, [allAnnotations]); useEffect(() => { let newSelectionType: | AnnotationBoundType | AnnotationImageToolType | AnnotationLinksBoundType; switch (boundModeSwitch) { case 'box': newSelectionType = annotationType; break; case 'link': newSelectionType = AnnotationLinksBoundType.chain; break; case 'segmentation': newSelectionType = 'polygon'; onChangeSelectedTool('pen'); break; default: newSelectionType = 'free-box'; } onChangeSelectionType(newSelectionType); }, [boundModeSwitch]); const isSaveButtonDisabled = useMemo(() => { if (isDocLabelsModified || documentLinksChanged) return false; return ( (isValidation && !splitValidation && touchedPages.length === 0) || ((!isValidation || splitValidation) && modifiedPages.length === 0) || !isAnnotatable || annotationSaved ); }, [ validPages, invalidPages, touchedPages, modifiedPages, editedPages, isDocLabelsModified, documentLinksChanged, annotationSaved ]); useEffect(() => { if (tableModeValues === 'cells') setIsCellMode(true); else setIsCellMode(false); }, [tableModeValues]); const SaveButton = ( <div className="flex flex-center"> <div className={ isSaveButtonDisabled ? styles['hot-key-container-disabled'] : styles['hot-key-container'] } > Ctrl + S </div> <span className={styles['custom-button']}>SAVE DRAFT</span> </div> ); const validationStyle = `${styles['validation-color']} flex flex-center ${ isValid ? styles.validColor : styles.invalidColor }`; const validationStatus: ValidationPageStatus = isValid ? 'Valid Page' : 'Invalid Page'; const jobId = useMemo(() => getJobId(), [getJobId]); useEffect(() => { if (categories) { const latestLabels: Label[] = categories .filter((category) => latestLabelsId?.includes(category.id)) .map((category) => { return { name: category.name, id: category.id }; }); setSelectedLabels(latestLabels); } }, [categories, latestLabelsId]); const dataAttrsWithTaxonomy = useMemo(() => { if (!selectedAnnotation || !categories) return; return getCategoryDataAttrs(selectedAnnotation?.category, categories)?.filter( (dataAttr) => dataAttr.type === 'taxonomy' ); }, [selectedAnnotation, categories]); const { data: taxonomy } = useLinkTaxonomyByCategoryAndJobId( { jobId, categoryId: selectedAnnotation?.category! }, { enabled: !!dataAttrsWithTaxonomy?.length } ); const cellsItems: { id: string; name: string; renderLabel: (el: any) => React.ReactNode; renderName: () => React.ReactNode; }[] = useMemo( () => categories ?.filter((el) => el.parent === 'table') .map((el) => ({ id: el.name, name: el.name, renderLabel: (el: any) => ( <span style={{ color: el.metadata?.color }} > {el.name} </span> ), // eslint-disable-next-line react/display-name renderName: () => ( <span style={{ color: el.metadata?.color }} > {el.name} </span> ) })) || [], [categories] ); const taskInfoElements = [ { name: 'Document:', value: `${fileMetaInfo.name}`, additional: ( <Copy className={styles.copyIcon} onClick={(e) => handleCopy(e, fileMetaInfo.name)} /> ) }, { name: 'TaskId:', value: `${task?.id}` }, { name: 'Pages:', value: `${task?.pages.join(', ')}` }, { name: 'Job Name:', value: `${task?.job.name}` }, { name: 'Deadline:', value: `${task?.deadline ?? ''}` }, { name: 'Status:', value: `${task?.status}` } ]; const taskInfo = ( <> {taskInfoElements.map((el) => ( <div className={styles['metadata-item']} key={el.name}> <span className={styles['metadata-item__name']}>{el.name}</span> <div className={styles.valueWrapper}> <span className={styles['metadata-item__value']}>{el.value}</span> {el.additional} </div> </div> ))} </> ); const docInfoElements = [ { name: 'Document ID:', value: `${fileMetaInfo.id}` }, { name: 'Pages:', value: `${fileMetaInfo.pages}` }, { name: 'Time:', value: `${fileMetaInfo.lastModified?.toString()}` } ]; const docInfo = ( <> {jobSettings} {docInfoElements.map((el) => ( <div className={styles['metadata-item']} key={el.name}> <span className={styles['metadata-item__name']}>{el.name}</span> <span className={styles['metadata-item__value']}>{el.value}</span> </div> ))} </> ); const handleSave = async () => { await onSaveTask(); onClearTouchedPages(); onClearModifiedPages(); clearAnnotationsChanges(); refetch(); }; const handleSaveEdits = async () => { onAddTouchedPage(); await onSaveEditClick(); refetch(); }; const handleToggleVisibility = () => { localStorage.setItem(VISIBILITY_SETTING_ID, String(!isHidden)); setIsHidden(!isHidden); }; const saveButtonTooltipContent = getSaveButtonTooltipContent( isSaveButtonDisabled, task?.status, isValidation ); const handleCellChange = (value: string) => { setCell(value); if (annotation) { const editedTableCells = annotation.tableCells?.map((tableCell: Annotation) => { let text = tableCell.text; if (tableCell?.id === currentCell?.id) { text = value; } return { ...tableCell, text }; }); onAnnotationEdited(currentPage, annotation.id, { tableCells: editedTableCells }); setTableCellsModified(true); } }; return ( <Panel cx={styles.wrapper}> <Button fill="none" cx={styles.hideIcon} onClick={handleToggleVisibility} icon={isHidden ? openIcon : closeIcon} /> {!isHidden && ( <div className={`${styles.container} flex-col`}> <div className={`${styles.main} flex-col`}> <FlexRow borderBottom="night50" background="none" cx="justify-center"> {TABS.map((tabName) => ( <TabButton size="36" key={tabName} caption={tabName} isLinkActive={tabValue === tabName} onClick={() => setTabValue(tabName)} /> ))} </FlexRow> <div className={`${styles.tabs} flex-col flex-cell`}> {!splitValidation && (isValid || isInvalid) ? ( <div className={validationStyle}> <Status statusTitle={ mapStatusForValidationPage(validationStatus).title } color={mapStatusForValidationPage(validationStatus).color} /> </div> ) : null} {tabValue === 'Categories' && ( <> <CategoriesTab boundModeSwitch={boundModeSwitch} setBoundModeSwitch={setBoundModeSwitch} /> {boundModeSwitch === 'segmentation' && ( <ImageToolsParams onChangeToolParams={(e) => { setSelectedToolParams({ type: selectedToolParams.type, values: e }); }} selectedTool={selectedTool} toolParams={selectedToolParams} /> )} {!viewMode && ( <CategoriesSelectionModeToggle selectionType={selectionType} onChangeSelectionType={onChangeSelectionType} selectionMode={boundModeSwitch} fileMetaInfo={fileMetaInfo} selectedTool={selectedTool} onChangeSelectedTool={onChangeSelectedTool} isDisabled={!isAnnotatable} /> )} </> )} {tabValue === 'Data' && tableMode && ( <> <div className={styles.switchLineCells}> <MultiSwitch items={[ { id: 'lines', caption: 'Lines' }, { id: 'cells', caption: 'Cells' } ]} value={tableModeValues} onValueChange={setTableModeValues} /> </div> {tableModeValues === 'lines' && ( <div className={styles.tableParams}> <LabeledInput label="Columns"> <NumericInput value={tableModeColumns} onValueChange={setTableModeColumns} min={1} max={20} /> </LabeledInput> <span>X</span> <LabeledInput label="Rows"> <NumericInput value={tableModeRows} onValueChange={setTableModeRows} min={1} max={20} /> </LabeledInput> </div> )} {tableModeValues === 'cells' && ( <div> <div className={styles.mergeButton}> <RadioGroup items={cellsItems} value={tableCellCategory} onValueChange={setTableCellCategory} direction="vertical" isDisabled={ !(cellsSelected && selectedCellsCanBeMerged) } /> </div> <div className={styles.mergeButton}> <Button color={'sky'} caption={'Merge'} icon={MergeIcon} isDisabled={ !(cellsSelected && selectedCellsCanBeMerged) } fill={'none'} onClick={() => onMergeCellsClicked(true)} /> </div> {cellsSelected && selectedCellsCanBeSplitted && ( <div className={styles.mergeButton}> <Button color={'sky'} caption={'Split'} icon={SplitIcon} fill={'none'} onClick={() => onSplitCellsClicked(true)} /> </div> )} </div> )} <div className={styles.cellInput}> <LabeledInput label="Value"> <TextInput value={cell} cx="c-m-t-5" onValueChange={handleCellChange} rawProps={{ style: { width: '200px' } }} /> </LabeledInput> </div> </> )} {tabValue === 'Data' && !tableMode && ( <TaskSidebarData annDataAttrs={annDataAttrs} selectedAnnotation={selectedAnnotation} isCategoryDataEmpty={isCategoryDataEmpty} onDataAttributesChange={onDataAttributesChange} viewMode={viewMode} onAnnotationEdited={onAnnotationEdited} currentPage={currentPage} taxonomyId={(taxonomy || [])[0]?.id} /> )} {tabValue === 'Information' && ( <div className={styles.information}> {task ? taskInfo : docInfo} </div> )} {tabValue === 'Document' && categories !== undefined && ( <TaskSidebarLabelsLinks viewMode={viewMode} jobId={jobId} onLabelsSelected={onLabelsSelected} selectedLabels={selectedLabels ?? []} documentLinks={documentLinks} onRelatedDocClick={onRelatedDocClick} selectedRelatedDoc={selectedRelatedDoc} /> )} {tabValue === 'Document' && categories === undefined && ( <NoData title="There are no categories" /> )} {isValidation && !splitValidation && ( <div className="flex justify-around"> {!editPage && ( <> <Button key={`valid${currentPage}`} cx={styles.validation} caption="Valid" fill={isValid ? undefined : 'none'} color="grass" onClick={ isValid ? undefined : () => { onValidClick(); onAddTouchedPage(); } } isDisabled={isValidationDisabled} /> {!isInvalid ? ( <Button key={`invalid${currentPage}`} cx={styles.validation} caption="Invalid" fill={isInvalid ? undefined : 'none'} color="fire" onClick={ isInvalid ? undefined : () => { onInvalidClick(); onAddTouchedPage(); } } isDisabled={isValidationDisabled} /> ) : ( <Button cx={styles.validation} caption="EDIT" fill="none" color="sky" onClick={onEditClick} isDisabled={isValidationDisabled} /> )} </> )} {editPage && !annotationSaved && ( <> <Button cx={styles.validation} caption="CANCEL" fill="none" color="sky" onClick={onCancelClick} isDisabled={isValidationDisabled} /> <Button cx={styles.validation} caption="SAVE EDITS" fill="none" color="sky" onClick={handleSaveEdits} isDisabled={isValidationDisabled} /> </> )} </div> )} </div> </div> {task && ( // todo: add "EDIT ANNOTATION" button here if no task <Tooltip content={saveButtonTooltipContent}> <Button caption={SaveButton} fill="white" onClick={handleSave} cx={styles.button} isDisabled={isSaveButtonDisabled} /> </Tooltip> )} <FinishButton viewMode={viewMode} isAnnotatable={isAnnotatable} allValidated={allValidated} isNextTaskPresented={isNextTaskPresented} isValidation={Boolean(isValidation)} isSplitValidation={Boolean(splitValidation)} notProcessedPages={notProcessedPages} onFinishValidation={onFinishValidation} onAnnotationTaskFinish={onAnnotationTaskFinish} onFinishSplitValidation={onFinishSplitValidation} onSaveTask={onSaveTask} jobType={job?.validation_type} taskStatus={task?.status} /> </div> )} </Panel> ); }; export default TaskSidebar;