diff --git a/web/src/api/hooks/tasks.ts b/web/src/api/hooks/tasks.ts index 2dc10ed0a..f0d03bb4d 100644 --- a/web/src/api/hooks/tasks.ts +++ b/web/src/api/hooks/tasks.ts @@ -71,10 +71,6 @@ export const useTasks: QueryHookType> = options ); -type TaskByIdParams = { - taskId: number; -}; - type UsersForTaskParams = { jobId: number; }; @@ -153,7 +149,7 @@ const mapTaskFromApi = (apiTask: ApiTask): Task => { return { ...task, user_id: user.id }; }; -export const useTaskById: QueryHookType = ({ taskId }) => +export const useTaskById: QueryHookType<{ taskId?: number }, Task> = ({ taskId }) => useQuery( ['task', taskId], async () => @@ -161,7 +157,7 @@ export const useTaskById: QueryHookType = ({ taskId }) => url: `${namespace}/tasks/${taskId}`, method: 'get' })(), - { select: mapTaskFromApi } + { select: mapTaskFromApi, enabled: Boolean(taskId) } ); export const useUsersForTask: QueryHookType = ({ jobId }) => useQuery(['usersForTask', jobId], async () => diff --git a/web/src/api/typings.ts b/web/src/api/typings.ts index ccfd91db8..173e6d51d 100644 --- a/web/src/api/typings.ts +++ b/web/src/api/typings.ts @@ -427,7 +427,7 @@ export type PageInfoObjs = { after?: string; data?: any; links?: Link[]; - children?: number[] | string[]; + children?: (number | string)[]; segments?: number[][]; original_annotation_id?: number; }; diff --git a/web/src/api/typings/jobs.ts b/web/src/api/typings/jobs.ts index 324cafb70..52427e4db 100644 --- a/web/src/api/typings/jobs.ts +++ b/web/src/api/typings/jobs.ts @@ -45,6 +45,12 @@ export type Job = { // TODO: check pipeline property }; +export type TJobUsers = { + owners: Job['owners']; + annotators: Job['annotators']; + validators: Job['validators']; +}; + export type JobType = | 'ExtractionWithAnnotationJob' | 'AnnotationJob' diff --git a/web/src/api/typings/tasks.ts b/web/src/api/typings/tasks.ts index 8a171425e..9ab1a6e90 100644 --- a/web/src/api/typings/tasks.ts +++ b/web/src/api/typings/tasks.ts @@ -1,5 +1,3 @@ -import { TUserShort } from 'api/typings'; - export type ApiTask = { id: number; status: TaskStatus; @@ -32,7 +30,7 @@ export type TaskModel = { file_id: number; pages: Array; job_id: number; - //todo: add dependecy from other models + //todo: add dependency from other models user_id: string; is_validation: boolean; deadline: string; @@ -56,5 +54,3 @@ export type TaskStats = { export type TaskStatus = 'Pending' | 'Ready' | 'In Progress' | 'Finished'; export type ValidationPageStatus = 'Valid Page' | 'Invalid Page'; - -export type TTaskUsers = Record<'owners' | 'validators' | 'annotators', TUserShort[]>; diff --git a/web/src/components/task/task-modal/task-validation-modal.tsx b/web/src/components/task/task-modal/task-validation-modal.tsx index 448db3c74..a61dec57d 100644 --- a/web/src/components/task/task-modal/task-validation-modal.tsx +++ b/web/src/components/task/task-modal/task-validation-modal.tsx @@ -20,7 +20,7 @@ import { IFormApi, IModal, Metadata, useArrayDataSource } from '@epam/uui'; import { TUserShort } from '../../../api/typings'; import styles from './task-modal.module.scss'; -import { TTaskUsers } from 'api/typings/tasks'; +import { TJobUsers } from 'api/typings/jobs'; import { DEFAULT_VALUES } from './constants'; export interface TaskValidationValues { @@ -32,9 +32,8 @@ interface IProps extends IModal { allValid: boolean; invalidPages: number; editedPageCount: number; - validSave: () => Promise; - allUsers: TTaskUsers; - currentUser: string; + validSave: () => void; + allUsers: TJobUsers; isOwner: boolean; onRedirectAfterFinish: () => void; } @@ -44,15 +43,16 @@ export const FinishTaskValidationModal: FC = (modalProps) => { allUsers, invalidPages, editedPageCount, - isOwner, allValid, + isOwner, abort, validSave, onRedirectAfterFinish, onSaveForm } = modalProps; + const [validatorUserId, onValidatorUserIdChange] = useState( - allUsers.validators[0]?.id || allUsers.annotators[0]?.id + allUsers.validators[0] || allUsers.annotators[0]?.id ); const [annotatorUserId, onAnnotatorUserIdChange] = useState(allUsers.annotators[0]?.id); diff --git a/web/src/components/task/task-sidebar-labels/task-sidebar-labels.tsx b/web/src/components/task/task-sidebar-labels/task-sidebar-labels.tsx index ed6dc5217..acc276c00 100644 --- a/web/src/components/task/task-sidebar-labels/task-sidebar-labels.tsx +++ b/web/src/components/task/task-sidebar-labels/task-sidebar-labels.tsx @@ -12,7 +12,7 @@ import { useLazyLoading } from 'shared/hooks/lazy-loading'; type TaskSidebarLabelsViewProps = { viewMode: boolean; - labels?: Category[]; + categories?: Category[]; onValueChange: (value: Label[]) => void; selectedLabels: Label[]; hasNextPage: boolean; @@ -22,9 +22,9 @@ type TaskSidebarLabelsViewProps = { const TaskSidebarLabelsView: FC = ({ viewMode = false, - labels, - onValueChange, + categories, selectedLabels, + onValueChange, hasNextPage, onFetchNext, isLoading @@ -37,7 +37,7 @@ const TaskSidebarLabelsView: FC = ({ const { task } = useTaskAnnotatorContext(); const isDisabled = task?.status !== 'In Progress' && task?.status !== 'Ready'; - if (!labels) { + if (!categories) { return ; } @@ -46,17 +46,17 @@ const TaskSidebarLabelsView: FC = ({ [selectedLabels] ); - const labelsArr = useMemo(() => labels.map(({ name, id }) => ({ name, id })), [labels]); + const labels = useMemo(() => categories.map(({ name, id }) => ({ name, id })), [categories]); const dataSource = useArrayDataSource( - { items: [...selectedLabels, ...labelsArr] }, - [labelsArr] + { items: [...selectedLabels, ...labels] }, + [labels] ); const renderData = viewMode || isDisabled ? ( - {labelsArr.map(({ id, name }) => ( + {labels.map(({ id, name }) => ( = ({ maxTotalItems={100} sorting={{ field: 'name', direction: 'asc' }} onValueChange={(value) => { - onValueChange(labelsArr.filter((item) => value?.includes(item.id))); + onValueChange(labels.filter((item) => value?.includes(item.id))); }} /> ); @@ -96,7 +96,7 @@ const TaskSidebarLabelsView: FC = ({ type TaskSidebarLabelsProps = { viewMode: boolean; - labels?: Category[]; + categories?: Category[]; onLabelsSelected: (labels: Label[]) => void; selectedLabels: Label[]; searchText: string; @@ -108,7 +108,7 @@ type TaskSidebarLabelsProps = { export const TaskSidebarLabels = ({ viewMode = false, - labels = [], + categories, onLabelsSelected, selectedLabels = [], searchText, @@ -128,10 +128,10 @@ export const TaskSidebarLabels = ({ size="24" placeholder="Search by label name" /> - {labels ? ( + {categories ? ( { onSelectTool('pen')} + onClick={() => onSelectTool(ToolNames.pen)} isDisabled={disabled} /> onSelectTool('brush')} + onClick={() => onSelectTool(ToolNames.brush)} isDisabled={disabled} /> onSelectTool('wand')} + onClick={() => onSelectTool(ToolNames.wand)} isDisabled={disabled} /> onSelectTool('eraser')} + onClick={() => onSelectTool(ToolNames.eraser)} isDisabled={disabled} /> onSelectTool('dextr')} + onClick={() => onSelectTool(ToolNames.dextr)} isDisabled={disabled} /> onSelectTool('rectangle')} + onClick={() => onSelectTool(ToolNames.rectangle)} isDisabled={disabled} /> onSelectTool('select')} + onClick={() => onSelectTool(ToolNames.select)} isDisabled={disabled} /> diff --git a/web/src/components/task/task-sidebar/task-sidebar-labels-links/task-sidebar-labels-links.tsx b/web/src/components/task/task-sidebar/task-sidebar-labels-links/task-sidebar-labels-links.tsx index 78fa8ebc3..61aa02869 100644 --- a/web/src/components/task/task-sidebar/task-sidebar-labels-links/task-sidebar-labels-links.tsx +++ b/web/src/components/task/task-sidebar/task-sidebar-labels-links/task-sidebar-labels-links.tsx @@ -98,18 +98,18 @@ export const TaskSidebarLabelsLinks: FC = ({ return ( + {documentCategoryType === DocumentCategoryType.Document ? ( = ({ jobSettings, viewMode, isNextTaskPr allValidated, annotationSaved, onFinishValidation, - notProcessedPages + notProcessedPages, + isDataTabDisabled } = useTaskAnnotatorContext(); const { tableModeColumns, @@ -151,7 +153,7 @@ const TaskSidebar: FC = ({ jobSettings, viewMode, isNextTaskPr break; case 'segmentation': newSelectionType = 'polygon'; - onChangeSelectedTool('pen'); + onChangeSelectedTool(ToolNames.pen); break; default: @@ -356,6 +358,7 @@ const TaskSidebar: FC = ({ jobSettings, viewMode, isNextTaskPr caption={tabName} isLinkActive={tabValue === tabName} onClick={() => setTabValue(tabName)} + isDisabled={tabName === 'Data' && isDataTabDisabled} /> ))} diff --git a/web/src/connectors/task-annotator-connector/task-annotator-context.tsx b/web/src/connectors/task-annotator-connector/task-annotator-context.tsx index eed24a380..8e8ac74f0 100644 --- a/web/src/connectors/task-annotator-connector/task-annotator-context.tsx +++ b/web/src/connectors/task-annotator-connector/task-annotator-context.tsx @@ -1,6 +1,5 @@ import React, { createContext, - MutableRefObject, useCallback, useContext, useEffect, @@ -8,14 +7,10 @@ import React, { useRef, useState } from 'react'; -import { cloneDeep, isEqual } from 'lodash'; -import { Task, TTaskUsers } from 'api/typings/tasks'; +import { cloneDeep } from 'lodash'; +import { Task } from 'api/typings/tasks'; import { ApiError } from 'api/api-error'; -import { - DocumentLink, - useAddAnnotationsMutation, - useLatestAnnotations -} from 'api/hooks/annotations'; +import { useAddAnnotationsMutation, useLatestAnnotations } from 'api/hooks/annotations'; import { useSetTaskFinishedMutation, useSetTaskState, useTaskById } from 'api/hooks/tasks'; import { useCategoriesByJob } from 'api/hooks/categories'; import { useDocuments } from 'api/hooks/documents'; @@ -25,8 +20,6 @@ import { Category, CategoryDataAttributeWithValue, ExternalViewerState, - FileDocument, - FilterWithDocumentExtraOption, Label, Link, Operators, @@ -37,7 +30,6 @@ import { import { Job } from 'api/typings/jobs'; import { FileMetaInfo } from 'pages/document/document-page-sidebar-content/document-page-sidebar-content'; -import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from 'react-query'; import { Annotation, AnnotationBoundType, @@ -47,7 +39,7 @@ import { PageToken, PaperToolParams, TableGutterMap, - toolNames + ToolNames } from 'shared'; import { useAnnotationsLinks } from 'shared/components/annotator/utils/use-annotation-links'; import { documentSearchResultMapper } from 'shared/helpers/document-search-result-mapper'; @@ -65,39 +57,35 @@ import { mapTokenPagesFromApi } from './task-annotator-utils'; import useSplitValidation, { SplitValidationValue } from './use-split-validation'; -import { useTaskUsers } from './use-task-users'; import { DocumentLinksValue, useDocumentLinks } from './use-document-links'; import { useValidation, ValidationValues } from './use-validation'; import { useNotifications } from 'shared/components/notifications'; import { Text, Panel } from '@epam/loveship'; import { getError } from 'shared/helpers/get-error'; +import { getToolsParams, removeAnnotationAndLabels } from './utils'; type ContextValue = SplitValidationValue & SyncScrollValue & DocumentLinksValue & - ValidationValues & { + Omit & { task?: Task; job?: Job; categories?: Category[]; - categoriesLoading?: boolean; selectedCategory?: Category; - selectedLink?: Link; selectedAnnotation?: Annotation; fileMetaInfo: FileMetaInfo; - tokensByPages: Record; + tokensByPages: Record; allAnnotations?: Record; pageNumbers: number[]; currentPage: number; modifiedPages: number[]; + isDataTabDisabled: boolean; pageSize?: { width: number; height: number }; setPageSize: (pS: any) => void; tabValue: string; - isOwner: boolean; - taskUsers: MutableRefObject; selectionType: AnnotationBoundType | AnnotationLinksBoundType | AnnotationImageToolType; selectedTool: AnnotationImageToolType; - setSelectedTool: (t: AnnotationImageToolType) => void; onChangeSelectedTool: (t: AnnotationImageToolType) => void; tableMode: boolean; isNeedToSaveTable: { @@ -108,15 +96,13 @@ type ContextValue = SplitValidationValue & gutters: Maybe; cells: Maybe; }) => void; - isDataTabDisabled: boolean; isCategoryDataEmpty: boolean; - annDataAttrs: Record>; + annDataAttrs: Record>; externalViewer: ExternalViewerState; onChangeSelectionType: ( newType: AnnotationBoundType | AnnotationLinksBoundType | AnnotationImageToolType ) => void; onCategorySelected: (category: Category) => void; - onLinkSelected: (link: Link) => void; onSaveTask: () => void; onExternalViewerClose: () => void; onAnnotationTaskFinish: () => void; @@ -150,7 +136,6 @@ type ContextValue = SplitValidationValue & latestLabelsId: string[]; isDocLabelsModified: boolean; getJobId: () => number | undefined; - linksFromApi?: DocumentLink[]; setCurrentDocumentUserId: (userId?: string) => void; currentDocumentUserId?: string; }; @@ -168,7 +153,6 @@ type ProviderProps = { type UndoListAction = 'edit' | 'delete' | 'add'; const TaskAnnotatorContext = createContext(undefined); -const dataTabDefaultDisableState = true; const defaultPageWidth: number = 0; const defaultPageHeight: number = 0; @@ -187,8 +171,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ const [selectedLabels, setSelectedLabels] = useState([]); const [latestLabelsId, setLatestLabelsId] = useState([]); const [isDocLabelsModified, setIsDocLabelsModified] = useState(false); - const [selectedLink, setSelectedLink] = useState(); - const [allAnnotations, setAllAnnotations] = useState>({}); + const [allAnnotations, setAllAnnotations] = useState>({}); const [copiedAnnotation, setCopiedAnnotation] = useState(); const copiedAnnotationReference = useRef(); @@ -210,12 +193,12 @@ export const TaskAnnotatorContextProvider: React.FC = ({ const [selectionType, setSelectionType] = useState< AnnotationBoundType | AnnotationLinksBoundType | AnnotationImageToolType >('free-box'); - const [selectedTool, setSelectedTool] = useState('pen'); + const [selectedTool, setSelectedTool] = useState(ToolNames.pen); const [selectedAnnotation, setSelectedAnnotation] = useState(); - const [isDataTabDisabled, setIsDataTabDisabled] = useState(dataTabDefaultDisableState); + const [isDataTabDisabled, setIsDataTabDisabled] = useState(true); const [isCategoryDataEmpty, setIsCategoryDataEmpty] = useState(false); const [annDataAttrs, setAnnDataAttrs] = useState< - Record> + Record> >({}); const [externalViewer, setExternalViewer] = useState(defaultExternalViewer); @@ -231,9 +214,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ cells: undefined }); - const [storedParams, setStoredParams] = useState<{ - [k in typeof toolNames[number]]: Maybe; - }>({ + const [storedParams, setStoredParams] = useState>>({ brush: undefined, dextr: undefined, eraser: undefined, @@ -243,7 +224,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ wand: undefined }); - let fileMetaInfo: FileMetaInfo = fileMetaInfoParam!; + let fileMetaInfo = fileMetaInfoParam!; const [pageSize, setPageSize] = useState<{ width: number; height: number }>({ width: defaultPageWidth, @@ -252,39 +233,12 @@ export const TaskAnnotatorContextProvider: React.FC = ({ const { notifyError } = useNotifications(); - let task: Task | undefined; - let isTaskLoading: boolean = false; - let refetchTask: ( - options?: (RefetchOptions & RefetchQueryFilters) | undefined - ) => Promise>; - if (taskId) { - const result = useTaskById({ taskId }, {}); - task = result.data; - isTaskLoading = result.isLoading; - refetchTask = result.refetch; - } - - const getJobId = (): number | undefined => (task ? task.job.id : jobId); - - const getFileId = (): number | undefined => (task ? task.file.id : fileMetaInfo?.id); - const { isOwner, taskUsers } = useTaskUsers(task); - + const { data: task, isLoading: isTaskLoading, refetch: refetchTask } = useTaskById({ taskId }); const { data: job } = useJobById({ jobId: task?.job.id }); - let pageNumbers: number[] = []; + const getJobId = (): number | undefined => (task ? task.job.id : jobId); - if (task) { - pageNumbers = task.pages; - } else if (fileMetaInfo?.pages) { - for (let i = 0; i < fileMetaInfo.pages; i++) { - pageNumbers.push(i + 1); - } - } - const { - data: { pages: categories } = {}, - refetch: refetchCategories, - isLoading: categoriesLoading - } = useCategoriesByJob( + const { data: { pages: categories } = {}, refetch: refetchCategories } = useCategoriesByJob( { jobId: getJobId(), size: 100, @@ -294,22 +248,37 @@ export const TaskAnnotatorContextProvider: React.FC = ({ { enabled: false } ); + const getFileId = (): number | undefined => (task ? task.file.id : fileMetaInfo?.id); + + const pageNumbers: number[] = useMemo(() => { + if (task) return task.pages; + if (fileMetaInfo?.pages) { + const pages = []; + for (let i = 0; i < fileMetaInfo.pages; i++) { + pages.push(i + 1); + } + + return pages; + } + + return []; + }, [task?.pages, fileMetaInfo?.pages]); + useEffect(() => { if (task?.job.id || jobId) { refetchCategories(); } }, [task, jobId]); - const documentFilters: FilterWithDocumentExtraOption[] = []; - - documentFilters.push({ - field: 'id', - operator: Operators.EQ, - value: getFileId() - }); const documentsResult = useDocuments( { - filters: documentFilters + filters: [ + { + field: 'id', + operator: Operators.EQ, + value: getFileId() + } + ] }, { enabled: false } ); @@ -353,6 +322,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ tokenRes.refetch(); } }, [task, job, revisionId]); + useAnnotationsLinks( selectedAnnotation, selectedCategory, @@ -363,6 +333,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ selectedAnnotation && onAnnotationEdited(prevPage, annId, links), setSelectedCategory ); + const createAnnotation = ( pageNum: number, annData: Annotation, @@ -387,7 +358,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ })); setModifiedPages((prevState) => { - return Array.from(new Set([...prevState, pageNum])); + return prevState.includes(pageNum) ? prevState : [...prevState, pageNum]; }); setTableMode(newAnnotation.boundType === 'table'); setSelectedAnnotation(newAnnotation); @@ -395,6 +366,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ setAnnotationDataAttrs(newAnnotation); return newAnnotation; }; + const onCloseDataTab = () => { setTabValue('Categories'); setIsDataTabDisabled(true); @@ -409,41 +381,20 @@ export const TaskAnnotatorContextProvider: React.FC = ({ }; const deleteAnnotation = (pageNum: number, annotationId: string | number) => { - const pageAnnotations = allAnnotations[pageNum] ?? []; - const anntn: Maybe = pageAnnotations.find((el) => el.id === annotationId); - if (anntn?.labels) { - const labelIdxToDelete = anntn.labels.findIndex( - (item) => item.annotationId === annotationId - ); - if (labelIdxToDelete !== -1) { - anntn?.labels?.splice(labelIdxToDelete, 1); - } - } + const pageAnnotations = allAnnotations[pageNum]; + const annotation: Maybe = pageAnnotations?.find((el) => el.id === annotationId); + + if (!annotation) return; + setAllAnnotations((prevState) => { - for (let k in prevState) { - prevState[k].map((annList) => - annList?.links?.filter((link) => link.to !== annotationId) - ); - } return { ...prevState, - [pageNum]: pageAnnotations.filter((ann) => { - if ( - anntn && - anntn.children && - anntn.boundType === 'table' && - (anntn.children as number[]).includes(+ann.id) && - ann.boundType === 'table_cell' - ) { - return false; - } - return ann.id !== annotationId; - }) + [pageNum]: removeAnnotationAndLabels(pageAnnotations, annotation) }; }); setModifiedPages((prevState) => { - return Array.from(new Set([...prevState, pageNum])); + return prevState.includes(pageNum) ? prevState : [...prevState, pageNum]; }); }; @@ -465,7 +416,8 @@ export const TaskAnnotatorContextProvider: React.FC = ({ if (!Array.isArray(labels)) return; const currentLabelsId = labels.map((label) => label.id); - const isDocLabelsModifiedNewVal = !isEqual(latestLabelsId, currentLabelsId); + const isDocLabelsModifiedNewVal = + latestLabelsId.toString() !== currentLabelsId.toString(); setIsDocLabelsModified(isDocLabelsModifiedNewVal); setSelectedLabels(labels); @@ -473,10 +425,6 @@ export const TaskAnnotatorContextProvider: React.FC = ({ [latestLabelsId] ); - const onLinkSelected = (link: Link) => { - setSelectedLink(link); - }; - const onChangeSelectionType = ( newType: AnnotationBoundType | AnnotationLinksBoundType | AnnotationImageToolType ) => { @@ -496,43 +444,10 @@ export const TaskAnnotatorContextProvider: React.FC = ({ }, [selectedToolParams]); useEffect(() => { - switch (selectedTool) { - case 'eraser': - if (storedParams.eraser) setSelectedToolParams(storedParams.eraser); - else - setSelectedToolParams({ - type: 'slider-number', - values: { - radius: { value: 40, bounds: { min: 0, max: 150 } } - } - }); - break; - case 'brush': - if (storedParams.brush) setSelectedToolParams(storedParams.brush); - else - setSelectedToolParams({ - type: 'slider-number', - values: { - radius: { value: 40, bounds: { min: 0, max: 150 } } - } - }); - break; - case 'wand': - if (storedParams.wand) setSelectedToolParams(storedParams.wand); - else - setSelectedToolParams({ - type: 'slider-number', - values: { - threshold: { value: 35, bounds: { min: 0, max: 150 } }, - deviation: { value: 15, bounds: { min: 0, max: 150 } } - } - }); - break; - case 'dextr': - case 'rectangle': - case 'select': - case 'pen': - break; + const toolParams = getToolsParams(selectedTool, storedParams); + + if (toolParams) { + setSelectedToolParams(toolParams); } }, [selectedTool]); @@ -554,7 +469,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ }; const onEmptyAreaClick = () => { - setIsDataTabDisabled(dataTabDefaultDisableState); + setIsDataTabDisabled(true); setIsCategoryDataEmpty(true); setTabValue('Categories'); setSelectedAnnotation(undefined); @@ -567,9 +482,9 @@ export const TaskAnnotatorContextProvider: React.FC = ({ ); if (foundCategoryDataAttrs && foundCategoryDataAttrs.length) { setAnnDataAttrs((prevState) => { - prevState[+annotation.id] = mapAnnDataAttrs( + prevState[annotation.id] = mapAnnDataAttrs( foundCategoryDataAttrs, - prevState[+annotation.id] + prevState[annotation.id] ); return prevState; }); @@ -580,7 +495,9 @@ export const TaskAnnotatorContextProvider: React.FC = ({ setTabValue('Categories'); setIsCategoryDataEmpty(true); } - setIsDataTabDisabled(foundCategoryDataAttrs && foundCategoryDataAttrs.length === 0); + setIsDataTabDisabled( + Boolean(foundCategoryDataAttrs && foundCategoryDataAttrs.length === 0) + ); }; useEffect(() => { @@ -618,12 +535,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ setAllAnnotations((prevState) => ({ ...prevState, - [pageNum]: [ - ...pageAnnotations, - { - ...newAnnotation - } - ] + [pageNum]: [...pageAnnotations, newAnnotation] })); }; @@ -639,8 +551,14 @@ export const TaskAnnotatorContextProvider: React.FC = ({ modifyAnnotation(pageNumber, annotationId, undoList[undoPointer].annotation); - const undoListCopy = cloneDeep(undoList); - undoListCopy[undoPointer].annotation = oldAnnotationState!; + if (!oldAnnotationState) return; + + const undoListCopy = [...undoList]; + undoListCopy[undoPointer] = { + ...undoListCopy[undoPointer], + annotation: oldAnnotationState + }; + setUndoList(undoListCopy); }; @@ -720,7 +638,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ const mapAttributes = mapAnnDataAttrs(foundCategoryDataAttrs, prevState[+id]); findAndSetExternalViewerType(mapAttributes); - prevState[+id] = mapAttributes; + prevState[id] = mapAttributes; return prevState; }); @@ -732,23 +650,23 @@ export const TaskAnnotatorContextProvider: React.FC = ({ } }; - const onDataAttributesChange = (elIndex: number, value: string) => { - const newAnn = { ...annDataAttrs }; + const onDataAttributesChange = (index: number, value: string) => { + if (!selectedAnnotation) return; - if (selectedAnnotation) { - const annItem = newAnn[+selectedAnnotation.id][elIndex]; - newAnn[+selectedAnnotation.id][elIndex].value = value; + const dataAttrByAnnotationId = [...annDataAttrs[selectedAnnotation.id]]; + const dataAttr = dataAttrByAnnotationId[index]; - if (isValidCategoryType(annItem.type)) { - setExternalViewer({ - isOpen: true, - type: annItem.type, - name: annItem.name, - value - }); - } - setAnnDataAttrs(newAnn); + dataAttrByAnnotationId[index] = { ...dataAttr, value }; + + if (isValidCategoryType(dataAttr.type)) { + setExternalViewer({ + isOpen: true, + type: dataAttr.type, + name: dataAttr.name, + value + }); } + setAnnDataAttrs({ ...annDataAttrs, [selectedAnnotation.id]: dataAttrByAnnotationId }); }; const addAnnotationMutation = useAddAnnotationsMutation(); @@ -759,8 +677,9 @@ export const TaskAnnotatorContextProvider: React.FC = ({ changes: Partial ) => { setAllAnnotations((prevState) => { - if (pageNum === -1) { - pageNum = (Object.keys(prevState) as unknown as Array).find((key: number) => + let pageNumber: string | number = pageNum; + if (pageNumber === -1) { + pageNumber = Object.keys(prevState).find((key: string) => prevState[key].find((ann) => ann.id == id) )!; } @@ -768,15 +687,12 @@ export const TaskAnnotatorContextProvider: React.FC = ({ return { ...prevState, [pageNum]: pageAnnotations.map((ann) => { - if (ann.id === id) { - return { ...ann, ...changes, id }; - } - return ann; + return ann.id !== id ? ann : { ...ann, ...changes }; }) }; }); setModifiedPages((prevState) => { - return Array.from(new Set([...prevState, pageNum])); + return prevState.includes(pageNum) ? prevState : [...prevState, pageNum]; }); }; @@ -786,20 +702,19 @@ export const TaskAnnotatorContextProvider: React.FC = ({ return { ...prevState, [pageNum]: pageAnnotations.map((ann) => { - if (ann.id === id) { - return { - ...ann, - links: ann.links?.filter((link) => { - return ( - link.category_id !== linkToDel.category_id && - link.page_num !== linkToDel.page_num && - link.to !== linkToDel.to && - link.type !== linkToDel.type - ); - }) - }; - } - return ann; + if (ann.id !== id) return ann; + + return { + ...ann, + links: ann.links?.filter((link) => { + return ( + link.category_id !== linkToDel.category_id && + link.page_num !== linkToDel.page_num && + link.to !== linkToDel.to && + link.type !== linkToDel.type + ); + }) + }; }) }; }); @@ -813,7 +728,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ if (!annotationBeforeModification) { return; } - const undoListCopy = cloneDeep(undoList); + const undoListCopy = [...undoList]; if (undoPointer !== -1) { undoListCopy.splice(undoPointer); // delete everything from pointer (including) to the right setUndoPointer(-1); @@ -836,21 +751,23 @@ export const TaskAnnotatorContextProvider: React.FC = ({ modifyAnnotation(pageNum, annotationId, changes); }; + const { linksToApi, setDocumentLinksChanged, ...documentLinksValues } = useDocumentLinks( + latestAnnotationsResult.data?.links_json + ); + const onSaveTask = async () => { if (!task || !latestAnnotationsResult.data) return; let { revision, pages } = latestAnnotationsResult.data; - const selectedLabelsId: string[] = selectedLabels.map((obj) => obj.id) ?? []; + const selectedLabelsId = selectedLabels.map((obj) => obj.id) ?? []; onCloseDataTab(); if (task.is_validation && !splitValidation.isSplitValidation) { - validationValues.setAnnotationSaved(true); + setAnnotationSaved(true); pages = pages.filter( - (page) => - validationValues.validPages.includes(page.page_num) || - validationValues.invalidPages.includes(page.page_num) + (page) => validPages.includes(page.page_num) || invalidPages.includes(page.page_num) ); } else { pages = mapModifiedAnnotationPagesToApi( @@ -878,9 +795,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ // } // TODO: del after BE will be ready (issue #569) - return validationValues.validPages.length || validationValues.invalidPages.length - ? [] - : pages; + return validPages.length || invalidPages.length ? [] : pages; }; try { @@ -889,21 +804,20 @@ export const TaskAnnotatorContextProvider: React.FC = ({ pages: getPages(), userId: task.user_id, revision, - validPages: validationValues.validPages, - invalidPages: validationValues.invalidPages, + validPages: validPages, + invalidPages: invalidPages, selectedLabelsId, - links: documentLinksValues.linksToApi + links: linksToApi }); onSaveTaskSuccess(); latestAnnotationsResult.refetch(); refetchTask(); - documentLinksValues?.setDocumentLinksChanged?.(false); + setDocumentLinksChanged?.(false); } catch (error) { onSaveTaskError(error as ApiError); } }; - - const tokensByPages = useMemo>(() => { + const tokensByPages = useMemo>(() => { if (!tokenPages?.length) { return {}; } @@ -914,12 +828,22 @@ export const TaskAnnotatorContextProvider: React.FC = ({ return mapTokenPagesFromApi(tokenPages, tokenScale); }, [tokenPages, pageSize]); - const validationValues = useValidation({ + const { + validPages, + invalidPages, + onAddTouchedPage, + setValidPages, + setAnnotationSaved, + ...validationValues + } = useValidation({ latestAnnotationsResult, task, currentPage, - taskUsers, - isOwner, + jobUsers: { + owners: job?.owners ?? [], + annotators: job?.annotators ?? [], + validators: job?.validators ?? [] + }, onCloseDataTab, onSaveTask, allAnnotations, @@ -931,6 +855,7 @@ export const TaskAnnotatorContextProvider: React.FC = ({ onSaveTaskSuccess, onSaveTaskError }); + const finishTaskMutation = useSetTaskFinishedMutation(); const onAnnotationTaskFinish = async () => { if (task) { @@ -966,10 +891,10 @@ export const TaskAnnotatorContextProvider: React.FC = ({ validatorAnnotations: allAnnotations, onAnnotationCreated, onAnnotationEdited, - onAddTouchedPage: validationValues.onAddTouchedPage, + onAddTouchedPage: onAddTouchedPage, setSelectedAnnotation, - validPages: validationValues.validPages, - setValidPages: validationValues.setValidPages, + validPages: validPages, + setValidPages: setValidPages, onAnnotationTaskFinish, userId: task?.user_id, task: task @@ -1017,19 +942,13 @@ export const TaskAnnotatorContextProvider: React.FC = ({ const { SyncedContainer } = useSyncScroll(); - const linksFromApi = latestAnnotationsResult.data?.links_json; - - const documentLinksValues = useDocumentLinks(linksFromApi); - const value = useMemo(() => { return { task, job, getJobId, categories, - categoriesLoading, selectedCategory, - selectedLink, fileMetaInfo, tokensByPages, allAnnotations, @@ -1040,7 +959,6 @@ export const TaskAnnotatorContextProvider: React.FC = ({ modifiedPages, selectionType, selectedTool, - setSelectedTool, selectedToolParams, setSelectedToolParams, onChangeSelectedTool, @@ -1049,10 +967,8 @@ export const TaskAnnotatorContextProvider: React.FC = ({ setIsNeedToSaveTable, tabValue, selectedAnnotation, - taskUsers, - isOwner, - isDataTabDisabled, isCategoryDataEmpty, + isDataTabDisabled, annDataAttrs, externalViewer, tableCellCategory, @@ -1062,7 +978,6 @@ export const TaskAnnotatorContextProvider: React.FC = ({ onAnnotationEdited, onLinkDeleted, onCategorySelected, - onLinkSelected, onChangeSelectionType, onSaveTask, onAnnotationTaskFinish, @@ -1084,22 +999,22 @@ export const TaskAnnotatorContextProvider: React.FC = ({ isDocLabelsModified, setSelectedLabels, latestLabelsId, - setLatestLabelsId, - linksFromApi, setCurrentDocumentUserId, currentDocumentUserId, + validPages, + invalidPages, + onAddTouchedPage, + setValidPages, SyncedContainer, + ...validationValues, ...splitValidation, - ...documentLinksValues, - ...validationValues + ...documentLinksValues }; }, [ task, job, categories, - categoriesLoading, selectedCategory, - selectedLink, selectionType, selectedTool, fileMetaInfo, @@ -1114,16 +1029,17 @@ export const TaskAnnotatorContextProvider: React.FC = ({ annDataAttrs, externalViewer, tableCellCategory, - isDataTabDisabled, selectedToolParams, splitValidation, SyncedContainer, selectedLabels, latestLabelsId, - linksFromApi, documentLinksValues, latestAnnotationsResult, - validationValues, + validPages, + invalidPages, + onAddTouchedPage, + setValidPages, currentDocumentUserId ]); diff --git a/web/src/connectors/task-annotator-connector/use-document-links.ts b/web/src/connectors/task-annotator-connector/use-document-links.ts index f571b25b5..4a5dad4e6 100644 --- a/web/src/connectors/task-annotator-connector/use-document-links.ts +++ b/web/src/connectors/task-annotator-connector/use-document-links.ts @@ -82,7 +82,7 @@ export const useDocumentLinks = (linksFromApi?: DocumentLink[]): DocumentLinksVa selectedRelatedDoc, setDocumentLinksChanged }), - [documentLinks, documentLinksChanged, linksToApi, selectedRelatedDoc] + [documentLinks, documentLinksChanged, selectedRelatedDoc] ); }; diff --git a/web/src/connectors/task-annotator-connector/use-split-validation.ts b/web/src/connectors/task-annotator-connector/use-split-validation.ts index 310c57132..f695a0b20 100644 --- a/web/src/connectors/task-annotator-connector/use-split-validation.ts +++ b/web/src/connectors/task-annotator-connector/use-split-validation.ts @@ -121,10 +121,11 @@ export default function useSplitValidation({ }; copy.id = Date.now(); + if ( - validatorAnnotations[currentPage] - ?.map((el) => el.originalAnnotationId) - .includes(Number(originalAnn.id)) + validatorAnnotations[currentPage]?.some( + (item) => item.originalAnnotationId === Number(originalAnn.id) + ) ) { return; } @@ -194,7 +195,6 @@ export default function useSplitValidation({ () => ({ annotationsByUserId, isSplitValidation, - job, onSplitAnnotationSelected, onSplitLinkSelected, onFinishSplitValidation, diff --git a/web/src/connectors/task-annotator-connector/use-task-users.ts b/web/src/connectors/task-annotator-connector/use-task-users.ts deleted file mode 100644 index 71e11ce3d..000000000 --- a/web/src/connectors/task-annotator-connector/use-task-users.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { FilterWithDocumentExtraOption, Operators, TUserShort, User } from '../../api/typings'; -import { useJobById } from '../../api/hooks/jobs'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useUserByForJob } from '../../api/hooks/users'; -import { Task, TTaskUsers } from '../../api/typings/tasks'; -import { Job } from 'api/typings/jobs'; - -export const useTaskUsers = (task: Task | undefined) => { - const [isOwner, setIsOwner] = useState(false); - const usersFilters = useRef[]>([]); - const taskUsers = useRef({ - owners: [], - annotators: [], - validators: [] - }); - - const { data: job, refetch: refetchJob } = useJobById( - { jobId: task?.job?.id || 0 }, - { enabled: false } - ); - const { data: users } = useUserByForJob( - { page: 1, size: 15, filters: usersFilters.current }, - {} - ); - - useEffect(() => { - if (!task) return; - refetchJob(); - }, [task]); - - useEffect(() => { - if (!job) return; - - const { owners, validators } = job; - const usersArr = new Set([...validators, ...owners]); - usersFilters.current = [ - { - field: 'id', - operator: Operators.IN, - value: Array.from(usersArr) - } - ]; - - if (task && job.owners.includes(task.user_id)) { - setIsOwner(true); - } - }, [job, task]); - - useEffect(() => { - if (!users?.data.length || !job) return; - - taskUsers.current = { - annotators: job.annotators, - ...getOwnersAndValidators(users.data, job) - }; - }, [users?.data, job]); - - return useMemo( - () => ({ - isOwner, - taskUsers - }), - [task] - ); -}; - -const getOwnersAndValidators = (users: User[], job: Job) => - users.reduce( - (accumulator: Record<'owners' | 'validators', TUserShort[]>, { id, username }) => { - if (job.owners.includes(id)) accumulator.owners.push({ id, username }); - if (job.validators.includes(id)) accumulator.validators.push({ id, username }); - return accumulator; - }, - { owners: [], validators: [] } - ); diff --git a/web/src/connectors/task-annotator-connector/use-validation.tsx b/web/src/connectors/task-annotator-connector/use-validation.tsx index 2df3a787a..c9b620edb 100644 --- a/web/src/connectors/task-annotator-connector/use-validation.tsx +++ b/web/src/connectors/task-annotator-connector/use-validation.tsx @@ -1,16 +1,8 @@ -import React, { - Dispatch, - MutableRefObject, - SetStateAction, - useCallback, - useEffect, - useMemo, - useState -} from 'react'; +import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; import { UseQueryResult } from 'react-query'; import { isEmpty } from 'lodash'; -import { Task, TTaskUsers } from 'api/typings/tasks'; +import { Task } from 'api/typings/tasks'; import { CategoryDataAttributeWithValue, PageInfo } from 'api/typings'; import { AnnotationsResponse, useAddAnnotationsMutation } from 'api/hooks/annotations'; @@ -27,14 +19,14 @@ import { mapModifiedAnnotationPagesToApi } from './task-annotator-utils'; import { useUuiContext } from '@epam/uui'; import { showError } from 'shared/components/notifications'; import { getError } from 'shared/helpers/get-error'; +import { TJobUsers } from 'api/typings/jobs'; export type ValidationParams = { latestAnnotationsResult: UseQueryResult; task?: Task; currentPage: number; onCloseDataTab: () => void; - isOwner: boolean; - taskUsers: MutableRefObject; + jobUsers: TJobUsers; onSaveTask: () => void; allAnnotations: Record; tokensByPages: Record; @@ -52,7 +44,6 @@ export type ValidationValues = { editedPages: number[]; touchedPages: number[]; notProcessedPages: number[]; - allValid: boolean; allValidated: boolean; annotationSaved: boolean; onValidClick: () => void; @@ -71,8 +62,7 @@ export const useValidation = ({ latestAnnotationsResult, task, currentPage, - taskUsers, - isOwner, + jobUsers, onCloseDataTab, onSaveTask, allAnnotations, @@ -165,7 +155,7 @@ export const useValidation = ({ setAnnotationSaved(false); }, [invalidPages, validPages, currentPage, notProcessedPages]); - const onClearTouchedPages = useCallback(async () => { + const onClearTouchedPages = useCallback(() => { setTouchedPages([]); }, []); @@ -241,9 +231,8 @@ export const useValidation = ({ +): PaperToolParams | undefined => { + switch (selectedTool) { + case 'eraser': + if (storedParams.eraser) return storedParams.eraser; + else + return { + type: 'slider-number', + values: { + radius: { value: 40, bounds: { min: 0, max: 150 } } + } + }; + case 'brush': + if (storedParams.brush) return storedParams.brush; + else + return { + type: 'slider-number', + values: { + radius: { value: 40, bounds: { min: 0, max: 150 } } + } + }; + case 'wand': + if (storedParams.wand) return storedParams.wand; + else + return { + type: 'slider-number', + values: { + threshold: { value: 35, bounds: { min: 0, max: 150 } }, + deviation: { value: 15, bounds: { min: 0, max: 150 } } + } + }; + case 'dextr': + case 'rectangle': + case 'select': + case 'pen': + break; + } +}; + +export const mapCategoriesIdToCategories = (ids: string[], categories: Category[]) => + ids.reduce((accumulator: Label[], id) => { + const category = categories.find((item) => item.id === id); + if (category) accumulator.push({ id, name: category.name }); + + return accumulator; + }, []); + +export const removeAnnotationAndLabels = ( + annotations: Annotation[], + annotationToRemove: Annotation +) => + annotations.reduce((accumulator: Annotation[], annotation) => { + if ( + annotationToRemove && + annotationToRemove.children && + annotationToRemove.boundType === 'table' && + annotationToRemove.children.includes(annotation.id) && + annotation.boundType === 'table_cell' + ) { + return accumulator; + } + + if (annotation.id === annotationToRemove.id) return accumulator; + if (!annotation.labels?.length) { + accumulator.push(annotation); + return accumulator; + } + + const currentLabels = [...annotation.labels]; + + const labelIdxToDelete = currentLabels.findIndex( + (item) => item.annotationId === annotationToRemove.id + ); + if (labelIdxToDelete !== -1) { + currentLabels.splice(labelIdxToDelete, 1); + } + + accumulator.push({ ...annotation, labels: currentLabels }); + return accumulator; + }, []); diff --git a/web/src/shared/components/annotator/annotator.tsx b/web/src/shared/components/annotator/annotator.tsx index cfbfe3194..908e1d0a3 100644 --- a/web/src/shared/components/annotator/annotator.tsx +++ b/web/src/shared/components/annotator/annotator.tsx @@ -22,7 +22,8 @@ import { Bound, PageToken, PaperTool, - TokenStyle + TokenStyle, + ToolNames } from './typings'; import { TokensLayer } from './layers/tokens-layer/tokens-layer'; import { editableAnnotationRenderer } from './layers/annotations-layer/annotations-editable-renderer'; @@ -427,7 +428,7 @@ export const Annotator: FC = ({ if (selectionType === 'polygon') { const defaultActivePen = createImageTool( - 'pen', + ToolNames.pen, onAnnotationDeleted, selectedToolParams ); diff --git a/web/src/shared/components/annotator/components/image-tools/index.ts b/web/src/shared/components/annotator/components/image-tools/index.ts index cc96e2de9..8f04ccf35 100644 --- a/web/src/shared/components/annotator/components/image-tools/index.ts +++ b/web/src/shared/components/annotator/components/image-tools/index.ts @@ -1,10 +1,4 @@ -import { - AnnotationImageToolType, - Maybe, - PaperTool, - PaperToolParams, - toolNames -} from '../../typings'; +import { AnnotationImageToolType, Maybe, PaperTool, PaperToolParams } from '../../typings'; import { createPenTool } from './pen-tool'; import { createSelectTool } from './select-tool'; import { createBrushTool } from './brush-tool'; @@ -16,7 +10,6 @@ export const createImageTool = ( onDeleteHandler: any, params: PaperToolParams ): Maybe => { - if (!toolNames.includes(toolName)) return undefined; switch (toolName) { case 'pen': return createPenTool({ onDeleteHandler, params }); diff --git a/web/src/shared/components/annotator/typings.ts b/web/src/shared/components/annotator/typings.ts index 2a0dd6ff1..7f6d5623d 100644 --- a/web/src/shared/components/annotator/typings.ts +++ b/web/src/shared/components/annotator/typings.ts @@ -40,19 +40,19 @@ export type PaperTool = { selection?: paper.Path; params: PaperToolParams; }; -export const toolNames = [ - 'pen', - 'brush', - 'eraser', - 'wand', - 'dextr', - 'rectangle', - 'select' -] as const; -export type AnnotationImageToolType = typeof toolNames[number]; -export type AnnotationImageTool = { - [k in typeof toolNames[number]]: Maybe; -}; + +export enum ToolNames { + pen = 'pen', + brush = 'brush', + eraser = 'eraser', + wand = 'wand', + dextr = 'dextr', + rectangle = 'rectangle', + select = 'select' +} + +export type AnnotationImageToolType = ToolNames; +export type AnnotationImageTool = Record>; export enum AnnotationLinksBoundType { chain = 'Chain', @@ -80,7 +80,7 @@ export type Annotation = { table?: AnnotationTable; data?: any; //TODO?? tableCells?: Maybe; - children?: number[] | string[]; + children?: (number | string)[]; segments?: number[][]; labels?: AnnotationLabel[]; originalAnnotationId?: number; diff --git a/web/src/shared/hooks/use-annotations-mapper.ts b/web/src/shared/hooks/use-annotations-mapper.ts index a4c904413..42028c23d 100644 --- a/web/src/shared/hooks/use-annotations-mapper.ts +++ b/web/src/shared/hooks/use-annotations-mapper.ts @@ -69,7 +69,7 @@ export default function useAnnotationsMapper( return [label]; } const tokenKey: string = getTokenKey(pageKey, topRightToken); - const labels: AnnotationLabel[] = tokenLabelsMap.get(tokenKey) ?? []; + const labels: AnnotationLabel[] = [...(tokenLabelsMap.get(tokenKey) ?? [])]; labels.push(label); tokenLabelsMap.set(tokenKey, labels);