From cf2d072a4541879b8dbd0aa9e1dec0e0e7a050e6 Mon Sep 17 00:00:00 2001 From: Richard Keil <8680858+richartkeil@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:22:30 +0100 Subject: [PATCH 01/22] refactor: remove external dependency for layer sorting --- .../layers/draggable-group-list-item.tsx | 34 ++ .../layers/draggable-layer-list-item.tsx | 31 ++ .../editor/layers/group-list-item.tsx | 64 ++- .../src/components/editor/layers/layers.tsx | 452 ++++++++---------- .../editor/save-popup/save-popup.tsx | 2 +- .../annotation-groups/annotation-group.ts | 50 +- apps/editor/src/models/editor/document.ts | 34 +- apps/editor/src/models/editor/layers/layer.ts | 15 +- .../src/lib/types/editor/document.ts | 9 +- libs/ui-shared/src/lib/types/editor/layers.ts | 24 +- package.json | 1 - yarn.lock | 31 -- 12 files changed, 376 insertions(+), 371 deletions(-) create mode 100644 apps/editor/src/components/editor/layers/draggable-group-list-item.tsx create mode 100644 apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx diff --git a/apps/editor/src/components/editor/layers/draggable-group-list-item.tsx b/apps/editor/src/components/editor/layers/draggable-group-list-item.tsx new file mode 100644 index 000000000..4de1ac165 --- /dev/null +++ b/apps/editor/src/components/editor/layers/draggable-group-list-item.tsx @@ -0,0 +1,34 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { IAnnotationGroup, ILayer } from "@visian/ui-shared"; +import { observer } from "mobx-react-lite"; + +import { AnnotationGroupListItem } from "./group-list-item"; + +export const DraggableAnnotationGroupListItem = observer<{ + group: IAnnotationGroup; + isActive: boolean; + isLast?: boolean; + isDragged?: boolean; + draggedLayer?: ILayer; +}>(({ group, isActive, isLast, isDragged, draggedLayer }) => { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: group.id, data: { annotationGroup: group } }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragged ? 0.3 : 1, + }; + + return ( +
+ +
+ ); +}); diff --git a/apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx b/apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx new file mode 100644 index 000000000..0a537fbb9 --- /dev/null +++ b/apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx @@ -0,0 +1,31 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { ILayer } from "@visian/ui-shared"; +import { observer } from "mobx-react-lite"; + +import { LayerListItem } from "./layer-list-item"; + +export const DraggableLayerListItem = observer<{ + layer: ILayer; + isActive?: boolean; + isLast?: boolean; + isDragged?: boolean; +}>(({ layer, isActive, isLast, isDragged }) => { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ + id: layer.id, + data: { layer }, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragged ? 0.3 : 1, + }; + + return ( +
+ +
+ ); +}); diff --git a/apps/editor/src/components/editor/layers/group-list-item.tsx b/apps/editor/src/components/editor/layers/group-list-item.tsx index 68e6ca03a..0dedd5bdd 100644 --- a/apps/editor/src/components/editor/layers/group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/group-list-item.tsx @@ -1,23 +1,65 @@ -import { FullWidthListItem, IAnnotationGroup } from "@visian/ui-shared"; +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { FullWidthListItem, IAnnotationGroup, ILayer } from "@visian/ui-shared"; import { observer } from "mobx-react-lite"; import { useCallback } from "react"; +import styled from "styled-components"; + +import { DraggableLayerListItem } from "./draggable-layer-list-item"; + +const ChildLayerContainer = styled.div` + margin-left: 16px; +`; export const AnnotationGroupListItem = observer<{ group: IAnnotationGroup; isActive: boolean; isLast?: boolean; -}>(({ group, isActive, isLast }) => { + draggedLayer?: ILayer; +}>(({ group, isActive, isLast, draggedLayer }) => { const toggleCollapse = useCallback(() => { - group.collapsed = !group.collapsed; + group.setCollapsed(!group.collapsed); }, [group]); + + const hideDividerForLayer = useCallback( + (layerIndex: number) => { + if (isLast && layerIndex === group.layers.length - 1) return true; + const indexOfActiveLayer = group.layers.findIndex((l) => l.isActive); + return layerIndex === indexOfActiveLayer - 1; + }, + [group, isLast], + ); + return ( - + <> + + {!group.collapsed && ( + + + {group.layers.map((layer, index) => ( + + ))} + + + )} + ); }); diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index ede28cc9e..ca9519dca 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -1,3 +1,21 @@ +import { + CollisionDetection, + DndContext, + DragEndEvent, + DragOverEvent, + DragOverlay, + DragStartEvent, + PointerSensor, + pointerWithin, + rectIntersection, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; import { computeStyleValue, FloatingUIButton, @@ -13,22 +31,18 @@ import { styledScrollbarMixin, SubtleText, } from "@visian/ui-shared"; -import { - SimpleTreeItemWrapper, - SortableTree, - TreeItem, - TreeItemComponentProps, - TreeItems, -} from "dnd-kit-sortable-tree"; -import { ItemChangedReason } from "dnd-kit-sortable-tree/dist/types"; +import { transaction } from "mobx"; import { observer } from "mobx-react-lite"; -import React, { ReactNode, useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; import styled from "styled-components"; import { useStore } from "../../../app/root-store"; import { ImageLayer } from "../../../models"; import { AnnotationGroup } from "../../../models/editor/annotation-groups"; import { InfoShortcuts } from "../info-shortcuts"; +import { DraggableAnnotationGroupListItem } from "./draggable-group-list-item"; +import { DraggableLayerListItem } from "./draggable-layer-list-item"; import { AnnotationGroupListItem } from "./group-list-item"; import { LayerListItem } from "./layer-list-item"; @@ -66,140 +80,36 @@ const LayerModal = styled(Modal)` justify-content: center; `; -interface TreeItemStyleWrapperProps - extends TreeItemComponentProps { - passedRef: React.ForwardedRef; - children: ReactNode; -} +const customCollisionDetection: CollisionDetection = (args) => { + const activeLayer = args.active.data.current?.layer as ILayer; + if (!activeLayer) return rectIntersection(args); -const TreeItemStyleWrapper = (props: TreeItemStyleWrapperProps) => { - const { passedRef, className, ...restProps } = props; - const newProps = { contentClassName: className, ...restProps }; - return ( - - ); -}; - -const indentationWidth = 20; -const treeWidth = 235; - -const StyledTreeItem = styled(TreeItemStyleWrapper)` - padding: 0px; - border: none; - background: none; - width: 100%; - max-width: ${treeWidth}px; -`; + // Check if there is a collission with a group different from the + // one of the dragged layer: + const pointerCollisions = pointerWithin(args); + const groupCollission = pointerCollisions.find((collission) => { + const group = collission.data?.droppableContainer.data.current + .annotationGroup as IAnnotationGroup; + return group && group !== activeLayer.annotationGroup; + }); + if (groupCollission) return [groupCollission]; -type TreeItemData = { - value: string; + return pointerWithin(args); }; -const LayerItem = ({ id }: { id: string }) => { - const store = useStore(); - const hideLayerDivider = useCallback( - (element: ILayer | IAnnotationGroup) => { - if (!element) return false; - const flatRenderingOrder = - store?.editor.activeDocument?.flatRenderingOrder; - if (!flatRenderingOrder) return false; - const renderingOrder = store?.editor.activeDocument?.renderingOrder; - if (!renderingOrder) return false; - const layerIndex = flatRenderingOrder.indexOf(element); - if (layerIndex === flatRenderingOrder.length - 1) return true; - if ( - element instanceof AnnotationGroup && - element.collapsed && - renderingOrder.indexOf(element) === renderingOrder.length - 1 - ) { - return true; - } - const nextElement = flatRenderingOrder[layerIndex + 1]; - return ( - nextElement.isActive && - (nextElement instanceof AnnotationGroup ? nextElement.collapsed : true) - ); - }, - [ - store?.editor.activeDocument?.flatRenderingOrder, - store?.editor.activeDocument?.renderingOrder, - ], - ); - - const group = store?.editor.activeDocument?.getAnnotationGroup(id); - if (group) { - const isActive = !!group.collapsed && group.isActive; - return ( -
- -
- ); - } - const layer = store?.editor.activeDocument?.getLayer(id); - if (layer) { - return ( -
- -
- ); - } - return ( - - - - ); -}; - -const TreeItemComponent = React.forwardRef< - HTMLDivElement, - TreeItemComponentProps ->((props, ref) => ( - - - -)); - -const layerToTreeItemData = (layer: ILayer) => ({ - id: layer.id, - value: layer.id, - canHaveChildren: false, -}); - export const Layers: React.FC = observer(() => { const store = useStore(); + const document = store?.editor.activeDocument; // Menu State - const isModalOpen = Boolean(store?.editor.activeDocument?.showLayerMenu); + const isModalOpen = Boolean(document?.showLayerMenu); // Menu Positioning const [buttonRef, setButtonRef] = useState(null); // This is required to force an update when the view mode changes // (otherwise the layer menu stays fixed in place when switching the view mode) - const viewMode = store?.editor.activeDocument?.viewSettings.viewMode; + const viewMode = document?.viewSettings.viewMode; const [, setLastUpdatedViewMode] = useState(); useEffect(() => { setTimeout(() => { @@ -207,120 +117,142 @@ export const Layers: React.FC = observer(() => { }, 50); }, [viewMode]); - // This is required to force an update when the active layer changes and the layer view must change its position - // (otherwise the layer menu stays fixed in place when switching the active layer between image and annotation) - // eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions - store?.editor.activeDocument?.activeLayer; + const layers = document?.layers; + const layerIds = document?.renderingOrder.map((element) => element.id) || []; - const layers = store?.editor.activeDocument?.layers; - const canGroupHaveChildren = useCallback( - (group: IAnnotationGroup) => { - const callback = (draggedItem: TreeItem) => { - const layer = store?.editor.activeDocument?.getLayer(draggedItem.value); - if (store?.reviewStrategy && layer && !group.layers.includes(layer)) { - return false; - } + const [draggedLayer, setDraggedLayer] = useState(); + const [draggedGroup, setDraggedGroup] = useState(); - return !!layer; - }; - return callback; - }, - [store?.editor.activeDocument, store?.reviewStrategy], + const dndSensors = useSensors( + // Require the mouse to move before dragging so we capture normal clicks: + useSensor(PointerSensor, { + activationConstraint: { distance: 8 }, + }), ); - const getTreeItems = useCallback(() => { - const annotationGroupToTreeItemData = (group: IAnnotationGroup) => ({ - id: group.id, - value: group.id, - children: group.layers.map((layer) => layerToTreeItemData(layer)), - collapsed: group.collapsed, - canHaveChildren: canGroupHaveChildren(group), - disableSorting: false, - }); - - const renderingOrder = store?.editor.activeDocument?.renderingOrder; - if (!renderingOrder) { - return []; + // This handler is called when the user starts dragging a layer or group: + const dndDragStart = useCallback((event: DragStartEvent) => { + if (event.active.data.current?.layer) { + setDraggedLayer(event.active.data.current?.layer); + } else if (event.active.data.current?.annotationGroup) { + setDraggedGroup(event.active.data.current?.annotationGroup); } + }, []); - return renderingOrder.map((element) => { - if (element instanceof AnnotationGroup) { - const group = element as AnnotationGroup; - return annotationGroupToTreeItemData(group); - } - if (element instanceof ImageLayer) { - const layer = element as ImageLayer; - return layerToTreeItemData(layer); - } - return { id: "undefined", value: "undefined" }; - }); - }, [canGroupHaveChildren, store?.editor.activeDocument?.renderingOrder]); + // This handler is called when the user currently drags a layer or group. + // Be aware that the value in event.over is influenced by our custom + // collission detection stragegy. + const dndDragOver = useCallback( + (event: DragOverEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + const activeLayer = active.data.current?.layer as ILayer; - const treeItems = getTreeItems(); + // Return if we are not dragging a layer, if we are not dragging OVER a + // group or if we are dragging over the layer's own group: + if (!activeLayer) return; + const overGroup = over.data.current?.annotationGroup as IAnnotationGroup; + if (!overGroup) return; + if (activeLayer.annotationGroup?.id === overGroup.id) return; - const canRootHaveChildren = useCallback((item) => { - const layer = store?.editor.activeDocument?.getLayer(item.value); - if (!layer) return true; // layerFamilies can always be children of root - return layer.annotationGroup === undefined; - }, []); - const updateRenderingOrder = useCallback( - ( - newTreeItems: TreeItems, - change: ItemChangedReason, - ) => { - if (change.type === "removed") return; - if (change.type === "collapsed" || change.type === "expanded") { - const group = store?.editor.activeDocument?.getAnnotationGroup( - change.item.value, + // Move layer from its old group to the new one. + // Use a transaction to make sure that mobx only updates + // dependencies once we have completed the move: + transaction(() => { + (activeLayer.annotationGroup as AnnotationGroup)?.removeLayer( + activeLayer, ); - if (!group) return; - group.collapsed = change.item.collapsed; + if (!activeLayer.annotationGroup && document) { + document.removeLayerFromRootList(activeLayer); + } + (overGroup as AnnotationGroup).addLayer(activeLayer); + }); + }, + [document], + ); + + // This handler is called when the user lets go of a layer or group after dragging: + const dndDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + + if (!document || !over || active.id === over.id) { + setDraggedGroup(undefined); + setDraggedLayer(undefined); return; } - if (change.type === "dropped") { - const draggedLayer = store?.editor.activeDocument?.getLayer( - change.draggedItem.value, + + const dragged = active.data.current; + if (dragged?.annotationGroup) { + const group = dragged?.annotationGroup as IAnnotationGroup; + const oldIndex = document.renderingOrder.indexOf(group); + const newIndex = document.renderingOrder.indexOf( + over.data?.current?.annotationGroup, ); + const newLayerIds = arrayMove( + document.renderingOrder.map((item) => item.id), + oldIndex, + newIndex, + ); + document.setLayerIds(newLayerIds); + } else if (dragged?.layer) { + const layer = dragged?.layer as ILayer; + // Only re-sort the groups layers if + // - the current layer is in a group + // - the group has more than 1 layer + // - we are not just dragging into the group folder, but actually into the layers of the group if ( - store?.reviewStrategy && - draggedLayer && - (change.draggedFromParent - ? change.draggedFromParent.value - : undefined) !== - (change.droppedToParent ? change.droppedToParent.value : undefined) + layer.annotationGroup && + layer.annotationGroup.layers.length > 1 && + over.data?.current?.layer ) { - return; - } - newTreeItems.forEach((item, index) => { - const layer = store?.editor.activeDocument?.getLayer(item.value); - if (layer) { - layer?.setAnnotationGroup(undefined, index); - } - const group = store?.editor.activeDocument?.getAnnotationGroup( - item.value, + const oldIndex = layer.annotationGroup.layerIds.indexOf(layer.id); + const newIndex = layer.annotationGroup.layerIds.indexOf( + over.data?.current?.layer.id, ); - if (group) { - item.children?.forEach((childItem, childIndex) => { - const childLayer = store?.editor.activeDocument?.getLayer( - childItem.value, - ); - if (childLayer) { - childLayer.setAnnotationGroup(group.id, childIndex); - } - }); - group.collapsed = item.collapsed; - store?.editor.activeDocument?.addAnnotationGroup( - group as AnnotationGroup, - index, - ); - } - }); + const newLayerIds = arrayMove( + layer.annotationGroup.layerIds, + oldIndex, + newIndex, + ); + layer.annotationGroup.setLayerIds(newLayerIds); + } } + setDraggedLayer(undefined); + setDraggedGroup(undefined); }, - [store?.editor.activeDocument, store?.reviewStrategy], + [document], ); - const firstElement = store?.editor.activeDocument?.renderingOrder[0]; + const listItems = document?.renderingOrder.map((element, index) => { + if (element instanceof AnnotationGroup) { + const group = element as AnnotationGroup; + return ( + + ); + } + const layer = element as ImageLayer; + return ( + + ); + }); + + const firstElement = document?.renderingOrder[0]; const isHeaderDivideVisible = !( firstElement?.isActive && (firstElement instanceof AnnotationGroup ? firstElement.collapsed : true) @@ -341,7 +273,7 @@ export const Layers: React.FC = observer(() => { tooltipTx="layers" showTooltip={!isModalOpen} ref={setButtonRef} - onPointerDown={store?.editor.activeDocument?.toggleLayerMenu} + onPointerDown={document?.toggleLayerMenu} isActive={isModalOpen} /> { icon="plus" tooltipTx="add-annotation-layer" isDisabled={ - !store?.editor.activeDocument?.imageLayers?.length || - store?.editor.activeDocument?.imageLayers?.length >= - (store?.editor.activeDocument?.maxVisibleLayers || 0) - } - onPointerDown={ - store?.editor.activeDocument?.addNewAnnotationLayer + !document?.imageLayers?.length || + document?.imageLayers?.length >= + (document?.maxVisibleLayers || 0) } + onPointerDown={document?.addNewAnnotationLayer} /> } > - - false }} - indentationWidth={20} - /> - {layers.length === 0 ? ( - - - - ) : ( - false + + + + {listItems} + {layers.length === 0 ? ( + + + + ) : ( + false + )} + + + {createPortal( + + {draggedLayer ? ( + + ) : null} + {draggedGroup ? ( + + ) : null} + , + window.document.body, )} - + diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index e78ba9a58..21573c0e6 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -110,7 +110,7 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { }; } const groupLayers = layer.getAnnotationGroupLayers(); - groupLayers.forEach((l) => annotationGroup.addLayer(l.id)); + groupLayers.forEach((l) => annotationGroup.addLayer(l)); return annotationGroup; } }; diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index 17c668bb0..271931fc8 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -8,7 +8,13 @@ import { ISerializable, isMiaAnnotationMetadata, } from "@visian/utils"; -import { action, computed, makeObservable, observable } from "mobx"; +import { + action, + computed, + makeObservable, + observable, + transaction, +} from "mobx"; import { v4 as uuidv4 } from "uuid"; import { Document } from "../document"; @@ -17,8 +23,8 @@ import { ImageLayer } from "../layers"; export class AnnotationGroup implements IAnnotationGroup, ISerializable { + public layerIds: string[] = []; public excludeFromSnapshotTracking = ["document"]; - protected layerIds: string[] = []; public title = ""; public id!: string; public collapsed?: boolean; @@ -37,6 +43,8 @@ export class AnnotationGroup isActive: computed, metadata: observable, + setCollapsed: action, + setLayerIds: action, addLayer: action, removeLayer: action, trySetIsVerified: action, @@ -53,29 +61,25 @@ export class AnnotationGroup ); } - public addLayer(id: string, index?: number) { - const layer = this.document.getLayer(id); - if (!layer) return; - if (layer.annotationGroup !== this) { - layer.annotationGroup?.removeLayer(layer.id); - } - const oldIndex = this.layerIds.indexOf(layer.id); - if (oldIndex < 0 && index !== undefined) { - this.layerIds.splice(index, 0, layer.id); - } else if (oldIndex < 0 && index === undefined) { - this.layerIds.push(id); - } else if (index !== undefined && oldIndex !== index) { - this.layerIds.splice(index, 0, this.layerIds.splice(oldIndex, 1)[0]); - } - this.document.addLayer(layer, index); + public setCollapsed(value: boolean) { + this.collapsed = value; + } + + public setLayerIds(ids: string[]) { + this.layerIds = ids; + } + + public addLayer(layer: ILayer) { + transaction(() => { + this.setLayerIds([...this.layerIds, layer.id]); + // In case the layer was in the document layer list (i.e. not in a group) + // we also remove it from there: + this.document.removeLayerFromRootList(layer); + }); } - public removeLayer(id: string, index?: number) { - if (!this.layerIds.includes(id)) return; - this.layerIds = this.layerIds.filter((layerId) => layerId !== id); - const layer = this.document.getLayer(id); - if (!layer) return; - this.document.addLayer(layer, index); + public removeLayer(layer: ILayer) { + this.setLayerIds(this.layerIds.filter((layerId) => layerId !== layer.id)); } public get isActive() { diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index 054b480da..a9ce2e12f 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -105,8 +105,8 @@ export class Document protected activeLayerId?: string; protected measurementDisplayLayerId?: string; - protected layerMap: { [key: string]: ILayer }; - protected annotationGroupMap: { [key: string]: AnnotationGroup }; + protected layerMap: { [layerId: string]: ILayer }; + protected annotationGroupMap: { [annotionGroupId: string]: AnnotationGroup }; protected layerIds: string[]; public measurementType: MeasurementType = "volume"; @@ -190,7 +190,6 @@ export class Document title: computed, layers: computed, renderingOrder: computed, - flatRenderingOrder: computed, annotationGroups: computed, activeLayer: computed, measurementDisplayLayer: computed, @@ -201,6 +200,7 @@ export class Document maxVisibleLayers3d: computed, setTitle: action, + setLayerIds: action, setActiveLayer: action, setMeasurementDisplayLayer: action, setMeasurementType: action, @@ -239,7 +239,7 @@ export class Document } const { length } = this.layers; if (!length) return undefined; - const lastLayer = this.getLayer(this.layerIds[length - 1]); + const lastLayer = this.layers[this.layers.length - 1]; const layerMeta = lastLayer?.metadata; if (isMiaMetadata(layerMeta)) { return layerMeta?.dataUri?.split("/").pop(); @@ -260,6 +260,10 @@ export class Document return (this.renderer?.capabilities.maxTextures || 0) - generalTextures3d; } + public setLayerIds(layerIds: string[]) { + this.layerIds = layerIds; + } + public get layers(): ILayer[] { return this.layerIds.flatMap((id) => { if (this.annotationGroupMap[id]) { @@ -278,19 +282,6 @@ export class Document }); } - public get flatRenderingOrder(): (ILayer | IAnnotationGroup)[] { - return this.layerIds.flatMap((id) => { - const group = this.annotationGroupMap[id]; - if (group) { - const array: (ILayer | IAnnotationGroup)[] = [ - group as IAnnotationGroup, - ]; - return array.concat(group.layers); - } - return this.layerMap[id]; - }); - } - public get annotationGroups(): IAnnotationGroup[] { return this.layerIds .filter((id) => !!this.annotationGroupMap[id]) @@ -486,14 +477,15 @@ export class Document } }; + public removeLayerFromRootList = (layer: ILayer): void => { + this.setLayerIds(this.layerIds.filter((id) => id !== layer.id)); + }; + /** Toggles the type of the layer (annotation or not) and repositions it accordingly */ public toggleTypeAndRepositionLayer = (idOrLayer: string | ILayer): void => { const layerId = typeof idOrLayer === "string" ? idOrLayer : idOrLayer.id; const layer = this.getLayer(layerId); if (!layer) return; - if (layer.isAnnotation) { - layer.setAnnotationGroup(undefined); - } layer.setIsAnnotation(!layer.isAnnotation); }; @@ -1054,7 +1046,7 @@ export class Document const layer = this.getLayer(layerId); const group = this.getAnnotationGroupFromFile(file); if (layer && group) { - group?.addLayer(layer.id); + group.addLayer(layer); } } diff --git a/apps/editor/src/models/editor/layers/layer.ts b/apps/editor/src/models/editor/layers/layer.ts index a28fd30fb..f05cca26f 100644 --- a/apps/editor/src/models/editor/layers/layer.ts +++ b/apps/editor/src/models/editor/layers/layer.ts @@ -65,7 +65,6 @@ export class Layer implements ILayer, ISerializable { annotationGroup: computed, isActive: computed, - setAnnotationGroup: action, setIsAnnotation: action, setTitle: action, setBlendMode: action, @@ -106,20 +105,10 @@ export class Layer implements ILayer, ISerializable { public get annotationGroup(): IAnnotationGroup | undefined { return this.document.annotationGroups?.find((group) => - group.layers.includes(this), + group.layerIds.includes(this.id), ); } - public setAnnotationGroup(id?: string, index?: number): void { - if (!id) { - this.annotationGroup?.removeLayer(this.id, index); - this.document.addLayer(this, index); - return; - } - const newGroup = this.document.getAnnotationGroup(id); - newGroup?.addLayer(this.id, index); - } - public getAnnotationGroupLayers(): ILayer[] { return ( this.annotationGroup?.layers ?? this.document.getOrphanAnnotationLayers() @@ -194,7 +183,7 @@ export class Layer implements ILayer, ISerializable { } public delete() { - this.annotationGroup?.removeLayer?.(this.id); + this.annotationGroup?.removeLayer(this); if (this.document.layers.includes(this)) { this.document.deleteLayer(this.id); } diff --git a/libs/ui-shared/src/lib/types/editor/document.ts b/libs/ui-shared/src/lib/types/editor/document.ts index 98b182e92..029585369 100644 --- a/libs/ui-shared/src/lib/types/editor/document.ts +++ b/libs/ui-shared/src/lib/types/editor/document.ts @@ -35,12 +35,6 @@ export interface IDocument { * top-to-bottom */ renderingOrder: (ILayer | IAnnotationGroup)[]; - /** - * The document's layer and annotation group stack. - * This contains all layers and all annotation groups, sorted by their renderingOrder - * annotation groups are followed by the layers within that group top-to-bottom - */ - flatRenderingOrder: (ILayer | IAnnotationGroup)[]; /** `true` if the document holds three-dimensional layers. */ has3DLayers: boolean; @@ -124,6 +118,9 @@ export interface IDocument { /** Deletes a layer from the document. */ deleteLayer(idOrLayer: string | ILayer): void; + /** Remove a layer only from the root layer id list (e.g. to add it to a group next). */ + removeLayerFromRootList(layer: ILayer): void; + /** Returns the first color that is not yet used to color any layer. */ getFirstUnusedColor(): string; /** Returns the color to be used for, e.g., 3D region growing preview. */ diff --git a/libs/ui-shared/src/lib/types/editor/layers.ts b/libs/ui-shared/src/lib/types/editor/layers.ts index 5687e7500..b6cce34c4 100644 --- a/libs/ui-shared/src/lib/types/editor/layers.ts +++ b/libs/ui-shared/src/lib/types/editor/layers.ts @@ -117,12 +117,6 @@ export interface ILayer { setMetadata(value?: BackendMetadata): void; - /** Sets the layer's annotation group and moves it to the specified index within its local rendering order. - * A layer with an undefined annotation group is an orphan. - * If the layer is an orphan its local rendering order is the document renderingOrder. - */ - setAnnotationGroup(id: string | undefined, idx?: number): void; - getAnnotationGroupLayers(): ILayer[]; setIsAnnotation(value?: boolean): void; @@ -216,6 +210,8 @@ export interface IAnnotationGroup { title: string; /** The group's meta data. Usually the object from the DB */ metadata?: BackendMetadata; + /** All layer ids in the group. */ + layerIds: string[]; /** All layers in the group. */ layers: ILayer[]; /** Whether the group is currently collapsed in the layer view* */ @@ -224,16 +220,16 @@ export interface IAnnotationGroup { isActive: boolean; /** Returns `true` if the annotation group has changes. */ hasChanges: boolean; - /** Adds a layer with specified id to the group at the specified index, the layer is removed from its previous group. - * If no index and the layer is already in the group, the layer's position remains unchanged. - * If no index is specified and the layer is not part of the gropu, the layer is inserted at the beginning. */ - addLayer(id: string, index?: number): void; - /** Removes a layer from the group making it an orphan. - * After being removed, the layer is added to the document at the specified index. - */ - removeLayer(id: string, index?: number): void; + /** Adds a layer to the group. Also removes it from the document root. */ + addLayer(layer: ILayer): void; + /** Removes the layer from the group, if it is part of the group. */ + removeLayer(layer: ILayer): void; /** set verified if fam */ trySetIsVerified(value: boolean): void; + /** Sets the layer ids of the group, e.g. for sorting. */ + setLayerIds(ids: string[]): void; + /** Sets collapsed state of the group. */ + setCollapsed(value: boolean): void; toJSON(): AnnotationGroupSnapshot; } diff --git a/package.json b/package.json index eae606969..7923fd62e 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,6 @@ "axios": "^1.2.2", "core-js": "^3.6.5", "css-element-queries": "^1.2.3", - "dnd-kit-sortable-tree": "^0.1.73", "file-saver": "^2.0.5", "hotkeys-js": "^3.10.1", "i18next": "^19.8.4", diff --git a/yarn.lock b/yarn.lock index 2a0e6c85e..4f6d5a94a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10570,13 +10570,6 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^1.2.1": - version: 1.2.1 - resolution: "clsx@npm:1.2.1" - checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 - languageName: node - linkType: hard - "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -11877,22 +11870,6 @@ __metadata: languageName: node linkType: hard -"dnd-kit-sortable-tree@npm:^0.1.73": - version: 0.1.73 - resolution: "dnd-kit-sortable-tree@npm:0.1.73" - dependencies: - clsx: ^1.2.1 - react-merge-refs: ^2.0.1 - peerDependencies: - "@dnd-kit/core": ">=6.0.5" - "@dnd-kit/sortable": ">=7.0.1" - "@dnd-kit/utilities": ">=3.2.0" - react: ">=16" - react-dom: ">=16" - checksum: 86bec921ebb4484f03848fccac21654b9a98ef590978815c45b908a297d28faf88094093545e74387315a70a8f661c497d32987f34573e6cd2bd44aed0314cad - languageName: node - linkType: hard - "dns-equal@npm:^1.0.0": version: 1.0.0 resolution: "dns-equal@npm:1.0.0" @@ -20504,13 +20481,6 @@ __metadata: languageName: node linkType: hard -"react-merge-refs@npm:^2.0.1": - version: 2.0.2 - resolution: "react-merge-refs@npm:2.0.2" - checksum: 64758870d79ad52e6666d1d30cdecd5a72722edfd5c89808b41acdbd81a039f0c78b8b576f7ae247010468fc45cb57dd31f402693c64224439dbe0127f4389f3 - languageName: node - linkType: hard - "react-native-get-random-values@npm:^1.4.0": version: 1.8.0 resolution: "react-native-get-random-values@npm:1.8.0" @@ -24124,7 +24094,6 @@ __metadata: core-js: ^3.6.5 css-element-queries: ^1.2.3 cypress: ^10.7.0 - dnd-kit-sortable-tree: ^0.1.73 eslint: ~8.15.0 eslint-config-airbnb: ^19.0.4 eslint-config-prettier: 8.1.0 From edf8ab033e6ca5e5b8a72774c7c615c9f26cc486 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Wed, 13 Dec 2023 14:17:27 +0100 Subject: [PATCH 02/22] fix: Import annotation groups correctly --- apps/editor/src/models/editor/document.ts | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index a9ce2e12f..cd91eecfd 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -707,14 +707,25 @@ export class Document } else if (filteredFiles.name.endsWith(".zip")) { const zip = await Zip.fromZipFile(filteredFiles); const unzippedFiles = await zip.getAllFiles(); - await this.importFiles( - this.createAnnotationGroup( - unzippedFiles, + if ("annotationGroupId" in filteredFiles) { + const typedFilteredFiles = filteredFiles as FileWithAnnotationGroup; + const newUnzippedFiles = unzippedFiles.map((unzippedFile) => { + const newFile = unzippedFile as FileWithAnnotationGroup; + newFile.annotationGroupId = typedFilteredFiles.annotationGroupId; + newFile.metadata = typedFilteredFiles.metadata; + return newFile; + }); + await this.importFiles(newUnzippedFiles); + } else { + await this.importFiles( + this.createAnnotationGroup( + unzippedFiles, + filteredFiles.name, + this.getMetadataFromFile(filteredFiles), + ), filteredFiles.name, - this.getMetadataFromFile(filteredFiles), - ), - filteredFiles.name, - ); + ); + } return; } else if (filteredFiles.name.endsWith(".json")) { await readTrackingLog(filteredFiles, this); From 2dd3c821d9cbfd91dede7064bde9bb94845fb28d Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Thu, 14 Dec 2023 18:16:14 +0100 Subject: [PATCH 03/22] fix: Import for single Images --- .../editor/annotation-groups/annotation-group.ts | 1 + apps/editor/src/models/editor/document.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index 271931fc8..2d15c1d3b 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -70,6 +70,7 @@ export class AnnotationGroup } public addLayer(layer: ILayer) { + if (!layer.isAnnotation) return; transaction(() => { this.setLayerIds([...this.layerIds, layer.id]); // In case the layer was in the document layer list (i.e. not in a group) diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index cd91eecfd..8924a98eb 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -481,6 +481,10 @@ export class Document this.setLayerIds(this.layerIds.filter((id) => id !== layer.id)); }; + public removeAnnotationGroup = (groupId: string): void => { + this.setLayerIds(this.layerIds.filter((id) => id !== groupId)); + }; + /** Toggles the type of the layer (annotation or not) and repositions it accordingly */ public toggleTypeAndRepositionLayer = (idOrLayer: string | ILayer): void => { const layerId = typeof idOrLayer === "string" ? idOrLayer : idOrLayer.id; @@ -863,6 +867,8 @@ export class Document descriptionTx: "image-loading-error", }); } + + this.removeEmptyGroups(); } else { //! TODO: #513 // const numberOfAnnotations = uniqueValues.size - 1; @@ -1192,4 +1198,12 @@ export class Document this.history.redo(this.activeLayer.id); } } + + protected removeEmptyGroups() { + Object.values(this.annotationGroupMap).forEach((group) => { + if (group.layers.length <= 1) { + this.removeAnnotationGroup(group.id); + } + }); + } } From be5dd365b7cec07c24f2b7f693cad605819519e0 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Thu, 14 Dec 2023 18:18:38 +0100 Subject: [PATCH 04/22] fix: DnD for annotation groups and Image Layers --- .../src/components/editor/layers/layers.tsx | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index ca9519dca..82dec9514 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -118,7 +118,9 @@ export const Layers: React.FC = observer(() => { }, [viewMode]); const layers = document?.layers; - const layerIds = document?.renderingOrder.map((element) => element.id) || []; + const [layerIds, setLayerIds] = useState( + document?.renderingOrder.map((element) => element.id) || [], + ); const [draggedLayer, setDraggedLayer] = useState(); const [draggedGroup, setDraggedGroup] = useState(); @@ -147,10 +149,17 @@ export const Layers: React.FC = observer(() => { const { active, over } = event; if (!over || active.id === over.id) return; const activeLayer = active.data.current?.layer as ILayer; - - // Return if we are not dragging a layer, if we are not dragging OVER a - // group or if we are dragging over the layer's own group: + // Return if we are not dragging a layer: if (!activeLayer) return; + // Return if we are dragging image layer within or above annotationGroup + const activeLayerIndex = layers?.indexOf(activeLayer) || 0; + if ( + !activeLayer.isAnnotation || + !layers?.[activeLayerIndex - 1]?.isAnnotation + ) + return; + // Return if we are not dragging OVER a + // group or if we are dragging over the layer's own group: const overGroup = over.data.current?.annotationGroup as IAnnotationGroup; if (!overGroup) return; if (activeLayer.annotationGroup?.id === overGroup.id) return; @@ -168,7 +177,7 @@ export const Layers: React.FC = observer(() => { (overGroup as AnnotationGroup).addLayer(activeLayer); }); }, - [document], + [document, layers], ); // This handler is called when the user lets go of a layer or group after dragging: @@ -189,12 +198,14 @@ export const Layers: React.FC = observer(() => { const newIndex = document.renderingOrder.indexOf( over.data?.current?.annotationGroup, ); - const newLayerIds = arrayMove( - document.renderingOrder.map((item) => item.id), - oldIndex, - newIndex, - ); - document.setLayerIds(newLayerIds); + if (newIndex !== -1) { + const newLayerIds = arrayMove( + document.renderingOrder.map((item) => item.id), + oldIndex, + newIndex, + ); + document.setLayerIds(newLayerIds); + } } else if (dragged?.layer) { const layer = dragged?.layer as ILayer; // Only re-sort the groups layers if @@ -216,12 +227,23 @@ export const Layers: React.FC = observer(() => { newIndex, ); layer.annotationGroup.setLayerIds(newLayerIds); + } else if (!layer.annotationGroup && !layer.isAnnotation && layers) { + const oldIndex = layerIds.indexOf(layer.id); + const newIndex = layerIds.indexOf(over.data?.current?.layer?.id); + const draggedImageLayer = layers.find( + (imageLayer) => imageLayer.id === layerIds[oldIndex], + ); + if (draggedImageLayer && newIndex !== -1) { + document.addLayer(draggedImageLayer, newIndex); + const newLayerIds = arrayMove(layerIds, oldIndex, newIndex); + setLayerIds(newLayerIds); + } } } setDraggedLayer(undefined); setDraggedGroup(undefined); }, - [document], + [document, layerIds, layers], ); const listItems = document?.renderingOrder.map((element, index) => { From 21139d00605881bed4206590032722e7add6c402 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sat, 16 Dec 2023 21:05:05 +0100 Subject: [PATCH 05/22] Feat: Add new Annotation Group --- apps/editor/src/assets/de.json | 5 +++- apps/editor/src/assets/en.json | 5 +++- .../src/components/editor/layers/layers.tsx | 20 +++++++++++-- apps/editor/src/models/editor/document.ts | 30 ++++++++++++++++--- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index fb1025adc..75aea66c2 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -93,6 +93,7 @@ "layers": "Ebenen", "main-image-layer": "Haupt-Bildebene", "add-annotation-layer": "Neue Annotation hinzufügen", + "add-annotation-group": "Neue Annotationsgruppe hinzufügen", "no-layers": "Keine Ebenen geladen.", "untitled-layer": "Unbenannte Ebene", "layer-settings": "Ebeneneinstellungen", @@ -432,5 +433,7 @@ "review": "Überprüfen", "supervise": "Abnicken", - "review-description": "{{taskType}} der Annotationen des Bildes {{image}}." + "review-description": "{{taskType}} der Annotationen des Bildes {{image}}.", + + "new-group": "Neue Gruppe" } diff --git a/apps/editor/src/assets/en.json b/apps/editor/src/assets/en.json index 734edef64..b68ac85c2 100644 --- a/apps/editor/src/assets/en.json +++ b/apps/editor/src/assets/en.json @@ -93,6 +93,7 @@ "layers": "Layers", "main-image-layer": "Main Image Layer", "add-annotation-layer": "Add new annotation", + "add-annotation-group": "Add new annotation group", "no-layers": "No layers loaded.", "untitled-layer": "Untitled layer", "layer-settings": "Layer Settings", @@ -433,5 +434,7 @@ "review": "Review", "supervise": "Supervise", - "review-description": "{{taskType}} the annotations of {{image}}" + "review-description": "{{taskType}} the annotations of {{image}}", + + "new-group": "New group" } diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index 82dec9514..9f0284455 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -80,6 +80,10 @@ const LayerModal = styled(Modal)` justify-content: center; `; +const StyledModalHeaderButton = styled(ModalHeaderButton)` + margin-right: 10px; +`; + const customCollisionDetection: CollisionDetection = (args) => { const activeLayer = args.active.data.current?.layer as ILayer; if (!activeLayer) return rectIntersection(args); @@ -312,15 +316,27 @@ export const Layers: React.FC = observer(() => { } /> + {document.activeLayer?.annotationGroup && ( + = + (document?.maxVisibleLayers || 0) + } + onPointerDown={document?.addNewAnnotationLayer} + /> + )} = (document?.maxVisibleLayers || 0) } - onPointerDown={document?.addNewAnnotationLayer} + onPointerDown={() => document?.addNewAnnotationGroup()} /> } diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index 8924a98eb..79857b281 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -449,17 +449,39 @@ export class Document : defaultRegionGrowingPreviewColor; }; - public addNewAnnotationLayer = () => { + public addNewAnnotationGroup = (title?: string) => { if (!this.mainImageLayer) return; const annotationColor = this.getFirstUnusedColor(); + const annotationLayer = ImageLayer.fromNewAnnotationForImage( + this.mainImageLayer.image, + this, + annotationColor, + ); + this.addLayer(annotationLayer); + + const newTitle = title || "new-group"; + const annotationGroup = new AnnotationGroup({ title: newTitle }, this); + this.addAnnotationGroup(annotationGroup); + annotationGroup.addLayer(annotationLayer); + + this.setActiveLayer(annotationLayer); + + // Force switch to 2D if too many layers for 3D + this.viewSettings.setViewMode(this.viewSettings.viewMode); + }; + public addNewAnnotationLayer = () => { + if (!this.mainImageLayer) return; + + const annotationColor = this.getFirstUnusedColor(); const annotationLayer = ImageLayer.fromNewAnnotationForImage( this.mainImageLayer.image, this, annotationColor, ); this.addLayer(annotationLayer); + this.activeLayer?.annotationGroup?.addLayer(annotationLayer); this.setActiveLayer(annotationLayer); // Force switch to 2D if too many layers for 3D @@ -594,7 +616,7 @@ export class Document public finishBatchImport() { if (!this.layers.some((layer) => layer.isAnnotation)) { - this.addNewAnnotationLayer(); + this.addNewAnnotationGroup(this.mainImageLayer?.title || "new-group"); this.viewport2D.setMainViewType(); } this.context?.persist(); @@ -748,13 +770,13 @@ export class Document // return; // } - if (filteredFiles instanceof File) { + if (filteredFiles instanceof File && !isAnnotation) { this.createAnnotationGroup( [filteredFiles], filteredFiles.name, this.getMetadataFromFile(filteredFiles), ); - } else { + } else if (!(filteredFiles instanceof File) && !isAnnotation) { this.createAnnotationGroup(filteredFiles, name ?? uuidv4()); } let createdLayerId = ""; From 9b49968bc8c93294e1d4e133f928187692b7d2c7 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sat, 16 Dec 2023 23:03:59 +0100 Subject: [PATCH 06/22] feat: Add Annotation Groups via UI --- apps/editor/src/assets/de.json | 2 +- apps/editor/src/assets/en.json | 2 +- .../editor/layers/group-list-item.tsx | 87 ++++++++++++++++++- .../editor/save-popup/save-popup.tsx | 6 +- .../annotation-groups/annotation-group.ts | 19 ++-- apps/editor/src/models/editor/document.ts | 9 +- libs/ui-shared/src/lib/types/editor/layers.ts | 7 +- 7 files changed, 113 insertions(+), 19 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index 75aea66c2..b572d3502 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -435,5 +435,5 @@ "supervise": "Abnicken", "review-description": "{{taskType}} der Annotationen des Bildes {{image}}.", - "new-group": "Neue Gruppe" + "untitled-group": "Unbenannte Gruppe" } diff --git a/apps/editor/src/assets/en.json b/apps/editor/src/assets/en.json index b68ac85c2..3401bba09 100644 --- a/apps/editor/src/assets/en.json +++ b/apps/editor/src/assets/en.json @@ -436,5 +436,5 @@ "supervise": "Supervise", "review-description": "{{taskType}} the annotations of {{image}}", - "new-group": "New group" + "untitled-group": "Untitled group" } diff --git a/apps/editor/src/components/editor/layers/group-list-item.tsx b/apps/editor/src/components/editor/layers/group-list-item.tsx index 0dedd5bdd..6c0487968 100644 --- a/apps/editor/src/components/editor/layers/group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/group-list-item.tsx @@ -2,11 +2,23 @@ import { SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import { FullWidthListItem, IAnnotationGroup, ILayer } from "@visian/ui-shared"; +import { + ContextMenu, + ContextMenuItem, + FullWidthListItem, + IAnnotationGroup, + ILayer, + PointerButton, + useDoubleTap, + useForwardEvent, + useTranslation, +} from "@visian/ui-shared"; +import { Pixel } from "@visian/utils"; import { observer } from "mobx-react-lite"; -import { useCallback } from "react"; +import { useCallback, useEffect, useState } from "react"; import styled from "styled-components"; +import { useStore } from "../../../app/root-store"; import { DraggableLayerListItem } from "./draggable-layer-list-item"; const ChildLayerContainer = styled.div` @@ -19,6 +31,8 @@ export const AnnotationGroupListItem = observer<{ isLast?: boolean; draggedLayer?: ILayer; }>(({ group, isActive, isLast, draggedLayer }) => { + const store = useStore(); + const toggleCollapse = useCallback(() => { group.setCollapsed(!group.collapsed); }, [group]); @@ -32,15 +46,71 @@ export const AnnotationGroupListItem = observer<{ [group, isLast], ); + const startTap2 = useDoubleTap( + useCallback((event: React.PointerEvent) => { + if (event.pointerType === "mouse") return; + setContextMenuPosition({ x: event.clientX, y: event.clientY }); + }, []), + ); + const startTap = useForwardEvent(startTap2); + + // Context Menu + const [contextMenuPosition, setContextMenuPosition] = useState( + null, + ); + const closeContextMenu = useCallback(() => { + setContextMenuPosition(null); + }, []); + useEffect(() => { + setContextMenuPosition(null); + }, [store?.editor.activeDocument?.viewSettings.viewMode]); + + // Press Handler + const handlePointerDown = useCallback( + (event: React.PointerEvent) => { + if (event.button === PointerButton.LMB) { + startTap(event); + } else if (event.button === PointerButton.RMB) { + setContextMenuPosition({ x: event.clientX, y: event.clientY }); + } + }, + [startTap], + ); + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + // Layer Renaming Handling + const [isAnnotationGroupNameEditable, setIsAnnotationGroupNameEditable] = + useState(false); + + const startEditingAnnotationGroupName = useCallback( + (event: React.PointerEvent) => { + event.preventDefault(); + setIsAnnotationGroupNameEditable(true); + closeContextMenu(); + }, + [closeContextMenu], + ); + const stopEditingAnnotationGroupName = useCallback(() => { + setIsAnnotationGroupNameEditable(false); + }, []); + return ( <> {!group.collapsed && ( @@ -60,6 +130,17 @@ export const AnnotationGroupListItem = observer<{ )} + + + ); }); diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index 21573c0e6..1370bee1c 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -100,9 +100,9 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { const annotationGroup = new AnnotationGroup(undefined, document); document.addAnnotationGroup(annotationGroup); if (annotation) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - annotationGroup.title = - annotation.dataUri.split("/").pop() ?? "annotation group :)"; + annotationGroup.setTitle( + annotation.dataUri.split("/").pop() ?? "annotation group :)", + ); annotationGroup.metadata = { ...annotation, backend: "mia", diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index 2d15c1d3b..4ebe5fa3e 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -25,8 +25,9 @@ export class AnnotationGroup { public layerIds: string[] = []; public excludeFromSnapshotTracking = ["document"]; - public title = ""; public id!: string; + protected titleOverride?: string; + public collapsed?: boolean; public metadata?: BackendMetadata; @@ -36,13 +37,14 @@ export class AnnotationGroup ) { this.applySnapshot(snapshot); - makeObservable(this, { + makeObservable(this, { layerIds: observable, collapsed: observable, - title: observable, + titleOverride: observable, isActive: computed, metadata: observable, + setTitle: action, setCollapsed: action, setLayerIds: action, addLayer: action, @@ -60,6 +62,13 @@ export class AnnotationGroup (layer) => layer.kind === "image" && (layer as ImageLayer).hasChanges, ); } + public get title(): string | undefined { + return this.titleOverride; + } + + public setTitle = (value?: string): void => { + this.titleOverride = value; + }; public setCollapsed(value: boolean) { this.collapsed = value; @@ -93,7 +102,7 @@ export class AnnotationGroup public toJSON(): AnnotationGroupSnapshot { return { id: this.id, - title: this.title, + titleOverride: this.titleOverride, metadata: this.metadata ? { ...this.metadata } : undefined, layerIds: [...this.layerIds], }; @@ -104,7 +113,7 @@ export class AnnotationGroup ) { if (!snapshot) return; this.id = snapshot.id || uuidv4(); - this.title = snapshot.title || ""; + this.setTitle(snapshot?.titleOverride || ""); this.metadata = snapshot.metadata ? { ...snapshot.metadata } : undefined; this.layerIds = snapshot.layerIds || []; } diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index 79857b281..a2eb455f7 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -460,8 +460,7 @@ export class Document ); this.addLayer(annotationLayer); - const newTitle = title || "new-group"; - const annotationGroup = new AnnotationGroup({ title: newTitle }, this); + const annotationGroup = new AnnotationGroup({ titleOverride: title }, this); this.addAnnotationGroup(annotationGroup); annotationGroup.addLayer(annotationLayer); @@ -616,7 +615,9 @@ export class Document public finishBatchImport() { if (!this.layers.some((layer) => layer.isAnnotation)) { - this.addNewAnnotationGroup(this.mainImageLayer?.title || "new-group"); + this.addNewAnnotationGroup( + this.mainImageLayer?.title || "untitled-group", + ); this.viewport2D.setMainViewType(); } this.context?.persist(); @@ -1130,7 +1131,7 @@ export class Document "Cannot create a new group for file that already belongs to a group", ); } - const annotationGroup = new AnnotationGroup({ title }, this); + const annotationGroup = new AnnotationGroup({ titleOverride: title }, this); annotationGroup.metadata = groupMetadata; const filesWithGroup = files.map((f) => { diff --git a/libs/ui-shared/src/lib/types/editor/layers.ts b/libs/ui-shared/src/lib/types/editor/layers.ts index b6cce34c4..f1d9b7726 100644 --- a/libs/ui-shared/src/lib/types/editor/layers.ts +++ b/libs/ui-shared/src/lib/types/editor/layers.ts @@ -48,7 +48,8 @@ export interface LayerSnapshot { export interface AnnotationGroupSnapshot { id: string; - title: string; + + titleOverride?: string; metadata?: BackendMetadata; layerIds: string[]; } @@ -207,7 +208,7 @@ export interface IImageLayer extends ILayer { export interface IAnnotationGroup { id: string; /** The group's display name. */ - title: string; + title?: string; /** The group's meta data. Usually the object from the DB */ metadata?: BackendMetadata; /** All layer ids in the group. */ @@ -220,6 +221,8 @@ export interface IAnnotationGroup { isActive: boolean; /** Returns `true` if the annotation group has changes. */ hasChanges: boolean; + /** Sets the group's title. */ + setTitle(value?: string): void; /** Adds a layer to the group. Also removes it from the document root. */ addLayer(layer: ILayer): void; /** Removes the layer from the group, if it is part of the group. */ From d241ab4b214d106b12dab7f1681f7c8098f5182b Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Mon, 15 Jan 2024 20:35:32 +0100 Subject: [PATCH 07/22] Fix: Can not drag and drop after import --- .../src/components/editor/layers/layers.tsx | 9 ++++---- apps/editor/src/models/editor/document.ts | 23 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index 9f0284455..b119bb19a 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -33,7 +33,7 @@ import { } from "@visian/ui-shared"; import { transaction } from "mobx"; import { observer } from "mobx-react-lite"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import styled from "styled-components"; @@ -122,8 +122,9 @@ export const Layers: React.FC = observer(() => { }, [viewMode]); const layers = document?.layers; - const [layerIds, setLayerIds] = useState( - document?.renderingOrder.map((element) => element.id) || [], + const layerIds = useMemo( + () => document?.renderingOrder.map((element) => element.id) || [], + [document?.renderingOrder], ); const [draggedLayer, setDraggedLayer] = useState(); @@ -240,7 +241,7 @@ export const Layers: React.FC = observer(() => { if (draggedImageLayer && newIndex !== -1) { document.addLayer(draggedImageLayer, newIndex); const newLayerIds = arrayMove(layerIds, oldIndex, newIndex); - setLayerIds(newLayerIds); + document.setLayerIds(newLayerIds); } } } diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index 79857b281..aef41e9c7 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -503,7 +503,10 @@ export class Document this.setLayerIds(this.layerIds.filter((id) => id !== layer.id)); }; - public removeAnnotationGroup = (groupId: string): void => { + public removeAnnotationGroup = ( + idOrGroup: string | IAnnotationGroup, + ): void => { + const groupId = typeof idOrGroup === "string" ? idOrGroup : idOrGroup.id; this.setLayerIds(this.layerIds.filter((id) => id !== groupId)); }; @@ -770,14 +773,16 @@ export class Document // return; // } - if (filteredFiles instanceof File && !isAnnotation) { - this.createAnnotationGroup( - [filteredFiles], - filteredFiles.name, - this.getMetadataFromFile(filteredFiles), - ); - } else if (!(filteredFiles instanceof File) && !isAnnotation) { - this.createAnnotationGroup(filteredFiles, name ?? uuidv4()); + if (!isAnnotation) { + if (filteredFiles instanceof File) { + this.createAnnotationGroup( + [filteredFiles], + filteredFiles.name, + this.getMetadataFromFile(filteredFiles), + ); + } else { + this.createAnnotationGroup(filteredFiles, name ?? uuidv4()); + } } let createdLayerId = ""; const isFirstLayer = From 16940aeaeba1cbd85dc15c27542f06d501ec6c5b Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Tue, 16 Jan 2024 01:26:26 +0100 Subject: [PATCH 08/22] Feat: Delete AnnotationsGroups --- apps/editor/src/assets/de.json | 3 +- apps/editor/src/assets/en.json | 3 +- .../confirmation-popup.props.ts | 5 +- .../confirmation-popup/confirmation-popup.tsx | 6 ++- .../editor/layers/group-list-item.tsx | 46 +++++++++++++++++++ .../annotation-groups/annotation-group.ts | 6 +++ apps/editor/src/models/editor/document.ts | 16 ++++++- libs/ui-shared/src/lib/types/editor/layers.ts | 4 ++ 8 files changed, 83 insertions(+), 6 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index b572d3502..3ff16acdd 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -435,5 +435,6 @@ "supervise": "Abnicken", "review-description": "{{taskType}} der Annotationen des Bildes {{image}}.", - "untitled-group": "Unbenannte Gruppe" + "untitled-group": "Unbenannte Gruppe", + "delete-annotation-group-message": "Wollen Sie die Annotation \"{{name}}\" und die folgenden Ebenen wirklich löschen??" } diff --git a/apps/editor/src/assets/en.json b/apps/editor/src/assets/en.json index 3401bba09..984e3d0f9 100644 --- a/apps/editor/src/assets/en.json +++ b/apps/editor/src/assets/en.json @@ -436,5 +436,6 @@ "supervise": "Supervise", "review-description": "{{taskType}} the annotations of {{image}}", - "untitled-group": "Untitled group" + "untitled-group": "Untitled group", + "delete-annotation-group-message": "Do you really want to delete the annotation group \"{{name}}\" and following layers?" } diff --git a/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts b/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts index cc3e81c07..3f929549b 100644 --- a/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts +++ b/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts @@ -1,6 +1,8 @@ import type { StatefulPopUpProps } from "@visian/ui-shared"; +import { ReactNode } from "react"; -export interface ConfirmationPopUpProps extends StatefulPopUpProps { +export interface ConfirmationPopUpProps + extends StatefulPopUpProps { title?: string; titleTx?: string; message?: string; @@ -10,4 +12,5 @@ export interface ConfirmationPopUpProps extends StatefulPopUpProps { cancel?: string; cancelTx?: string; onConfirm?: () => void; + children?: T; } diff --git a/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.tsx b/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.tsx index b86ad502c..47d540665 100644 --- a/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.tsx +++ b/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.tsx @@ -1,6 +1,6 @@ import { ButtonParam, PopUp, Text } from "@visian/ui-shared"; import { observer } from "mobx-react-lite"; -import { useCallback } from "react"; +import { ReactNode, useCallback } from "react"; import styled from "styled-components"; import { ConfirmationPopUpProps } from "./confirmation-popup.props"; @@ -23,7 +23,7 @@ const ConfirmationPopupContainer = styled(PopUp)` width: 400px; `; -export const ConfirmationPopup = observer( +export const ConfirmationPopup = observer>( ({ isOpen, onClose, @@ -36,6 +36,7 @@ export const ConfirmationPopup = observer( confirmTx, cancel, cancelTx, + children, }) => { const handleConfirmation = useCallback(() => { onConfirm?.(); @@ -51,6 +52,7 @@ export const ConfirmationPopup = observer( shouldDismissOnOutsidePress > + {children} (({ group, isActive, isLast, draggedLayer }) => { const store = useStore(); + const { t } = useTranslation(); + const toggleCollapse = useCallback(() => { group.setCollapsed(!group.collapsed); }, [group]); @@ -97,6 +106,26 @@ export const AnnotationGroupListItem = observer<{ setIsAnnotationGroupNameEditable(false); }, []); + // Delete annotation confirmation popup + const [ + isDeleteConfirmationPopUpOpen, + openDeleteConfirmationPopUp, + closeDeleteConfirmationPopUp, + ] = usePopUpState(false); + + const deleteAnnotationGroup = useCallback(() => { + if (group.metadata) { + // message popup is not able to delete + } else { + openDeleteConfirmationPopUp(); + } + }, [group.metadata, openDeleteConfirmationPopUp]); + + const handleDeletionConfirmation = useCallback(() => { + group.delete(); + setContextMenuPosition(null); + }, [group]); + return ( <> + + + + + + ); }); diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index 4ebe5fa3e..eee393dea 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -126,4 +126,10 @@ export class AnnotationGroup }; } } + + public delete() { + if (this.document.annotationGroups.includes(this)) { + this.document.deleteAnnotationGroup(this); + } + } } diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index a2eb455f7..82f256d78 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -494,10 +494,24 @@ export class Document this.layerMap[layerId].delete(); delete this.layerMap[layerId]; if (this.activeLayerId === layerId) { - this.setActiveLayer(this.layerIds[0]); + this.setActiveLayer(this.layers[0]); } }; + public deleteAnnotationGroup = (annotationGroup: IAnnotationGroup): void => { + // Remove the annotationGroupId from layerIds + this.layerIds = this.layerIds.filter((id) => id !== annotationGroup.id); + + // Delete all layers from the annotation group + annotationGroup.layerIds.forEach((id) => this.deleteLayer(id)); + + if (this.activeLayerId === annotationGroup.id) { + this.setActiveLayer(this.layers[0]); + } + + delete this.annotationGroupMap[annotationGroup.id]; + }; + public removeLayerFromRootList = (layer: ILayer): void => { this.setLayerIds(this.layerIds.filter((id) => id !== layer.id)); }; diff --git a/libs/ui-shared/src/lib/types/editor/layers.ts b/libs/ui-shared/src/lib/types/editor/layers.ts index f1d9b7726..347fc392e 100644 --- a/libs/ui-shared/src/lib/types/editor/layers.ts +++ b/libs/ui-shared/src/lib/types/editor/layers.ts @@ -234,5 +234,9 @@ export interface IAnnotationGroup { /** Sets collapsed state of the group. */ setCollapsed(value: boolean): void; + /** Delete this annotation group from the document and all + * the layers it contains. */ + delete(): void; + toJSON(): AnnotationGroupSnapshot; } From 455f90adea5838a2e545f8c8555abd1b642d3892 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sat, 20 Jan 2024 19:39:23 +0100 Subject: [PATCH 09/22] Feat: Delete annotation references from database --- apps/editor/src/assets/de.json | 5 +- apps/editor/src/assets/en.json | 5 +- .../editor/layers/group-list-item.tsx | 52 ++++++++++++++++--- .../src/components/editor/layers/layers.tsx | 6 +-- 4 files changed, 57 insertions(+), 11 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index 3ff16acdd..98a2d4775 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -436,5 +436,8 @@ "review-description": "{{taskType}} der Annotationen des Bildes {{image}}.", "untitled-group": "Unbenannte Gruppe", - "delete-annotation-group-message": "Wollen Sie die Annotation \"{{name}}\" und die folgenden Ebenen wirklich löschen??" + "delete-annotation-group-message": "Wollen Sie die Annotation \"{{name}}\" und die folgenden Ebenen wirklich löschen??", + "delete-backend-data-warning": "Warnung: Durch das Löschen der Annotationsgruppe \"{{name}}\" werden auch bereits unter \"{{dataUri}}\" gespeicherte Daten unwiederruflich gelöscht!", + "get-annotation-error": "Annotation konnte nicht gefunden werden", + "get-annotation-error-description": "Die Annotation mit der ID \"{{id}}\" konnte nicht gefunden werden." } diff --git a/apps/editor/src/assets/en.json b/apps/editor/src/assets/en.json index 984e3d0f9..8a2c9f49f 100644 --- a/apps/editor/src/assets/en.json +++ b/apps/editor/src/assets/en.json @@ -437,5 +437,8 @@ "review-description": "{{taskType}} the annotations of {{image}}", "untitled-group": "Untitled group", - "delete-annotation-group-message": "Do you really want to delete the annotation group \"{{name}}\" and following layers?" + "delete-annotation-group-message": "Do you really want to delete the annotation group \"{{name}}\" and following layers?", + "delete-backend-data-warning": "Warning: Deleting the annotation group \"{{name}}\" also irrevocably deletes data already saved under \"{{dataUri}}\"!", + "get-annotation-error": "Annotation can not be found", + "get-annotation-error-description": "Annotation with the ID \"{{id}}\" can not be found." } diff --git a/apps/editor/src/components/editor/layers/group-list-item.tsx b/apps/editor/src/components/editor/layers/group-list-item.tsx index 41fac09a3..994c93459 100644 --- a/apps/editor/src/components/editor/layers/group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/group-list-item.tsx @@ -15,12 +15,16 @@ import { useForwardEvent, useTranslation, } from "@visian/ui-shared"; -import { Pixel } from "@visian/utils"; +import { isMiaAnnotationMetadata, MiaAnnotation, Pixel } from "@visian/utils"; import { observer } from "mobx-react-lite"; import { useCallback, useEffect, useState } from "react"; import styled from "styled-components"; import { useStore } from "../../../app/root-store"; +import { + getAnnotation, + useDeleteAnnotationsForImageMutation, +} from "../../../queries"; import { ConfirmationPopup, usePopUpState } from "../../data-manager"; import { DraggableLayerListItem } from "./draggable-layer-list-item"; @@ -113,18 +117,46 @@ export const AnnotationGroupListItem = observer<{ closeDeleteConfirmationPopUp, ] = usePopUpState(false); - const deleteAnnotationGroup = useCallback(() => { - if (group.metadata) { - // message popup is not able to delete + const { deleteAnnotations } = useDeleteAnnotationsForImageMutation(); + const [miaAnnotationToBeDeleted, setMiaAnnotationToBeDeleted] = + useState(); + + const deleteAnnotationGroup = useCallback(async () => { + if (group.metadata && isMiaAnnotationMetadata(group.metadata)) { + try { + const miaAnnotation = await getAnnotation(group.metadata.id); + setMiaAnnotationToBeDeleted(miaAnnotation); + openDeleteConfirmationPopUp(); + } catch (error) { + store?.setError({ + titleTx: "get-annotation-error", + description: t("get-annotation-error-description", { + id: miaAnnotationToBeDeleted?.id, + }), + }); + } } else { openDeleteConfirmationPopUp(); } - }, [group.metadata, openDeleteConfirmationPopUp]); + }, [ + group.metadata, + miaAnnotationToBeDeleted?.id, + openDeleteConfirmationPopUp, + store, + t, + ]); const handleDeletionConfirmation = useCallback(() => { + if (miaAnnotationToBeDeleted) { + deleteAnnotations({ + imageId: miaAnnotationToBeDeleted?.image, + annotationIds: [miaAnnotationToBeDeleted?.id], + }); + setMiaAnnotationToBeDeleted(undefined); + } group.delete(); setContextMenuPosition(null); - }, [group]); + }, [deleteAnnotations, group, miaAnnotationToBeDeleted]); return ( <> @@ -186,6 +218,14 @@ export const AnnotationGroupListItem = observer<{ + {group.metadata && ( + + )} ); diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index b119bb19a..ca040157d 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -55,7 +55,7 @@ const OuterWrapper = styled("div")` width: 100%; `; -const LayerList = styled(List)` +const StyledLayerList = styled(List)` ${styledScrollbarMixin} margin-top: -16px; @@ -354,7 +354,7 @@ export const Layers: React.FC = observer(() => { items={layerIds} strategy={verticalListSortingStrategy} > - + {listItems} {layers.length === 0 ? ( @@ -363,7 +363,7 @@ export const Layers: React.FC = observer(() => { ) : ( false )} - + {createPortal( From bee5af2b3245d819a5f04ea5ab43fd5223dbf5dd Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sat, 20 Jan 2024 19:40:47 +0100 Subject: [PATCH 10/22] Feat: Refactor saveAs method, so that its add metadata --- .../editor/save-popup/save-popup.tsx | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index 1370bee1c..75b051622 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -1,6 +1,7 @@ import { Button, DropDown, + IAnnotationGroup, ILayer, LayerList, PopUp, @@ -23,7 +24,6 @@ import styled from "styled-components"; import { useStore } from "../../../app/root-store"; import { importFilesToDocument } from "../../../import-handling"; -import { AnnotationGroup } from "../../../models/editor/annotation-groups"; import { MiaReviewTask } from "../../../models/review-strategy"; import { fetchAnnotationFile } from "../../../queries"; import { SavePopUpProps } from "./save-popup.props"; @@ -91,28 +91,22 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { [newAnnotationURIPrefix, selectedExtension], ); - const createGroupForNewAnnotation = ( - layer: ILayer | undefined, + const addMetadataToGroup = ( + annotationGroup: IAnnotationGroup | undefined, annotation: MiaAnnotation | undefined, ) => { const document = store?.editor.activeDocument; - if (document && layer) { - const annotationGroup = new AnnotationGroup(undefined, document); - document.addAnnotationGroup(annotationGroup); - if (annotation) { - annotationGroup.setTitle( - annotation.dataUri.split("/").pop() ?? "annotation group :)", - ); - annotationGroup.metadata = { - ...annotation, - backend: "mia", - kind: "annotation", - }; - } - const groupLayers = layer.getAnnotationGroupLayers(); - groupLayers.forEach((l) => annotationGroup.addLayer(l)); - return annotationGroup; + if (document && annotationGroup && annotation) { + annotationGroup.metadata = { + ...annotation, + backend: "mia", + kind: "annotation", + }; + annotationGroup.layers.forEach((l) => { + l.metadata = { ...l, backend: "mia", kind: "annotation" }; + }); } + return annotationGroup; }; const createFileForAnnotationGroupOf = async ( @@ -251,7 +245,7 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { reviewTask.getAnnotation(newAnnotationId)! : annotationMeta; if (!annotationMeta) { - createGroupForNewAnnotation(activeLayer, newAnnotationMeta); + addMetadataToGroup(activeLayer?.annotationGroup, newAnnotationMeta); } else { await loadOldAnnotation(newAnnotationMeta, annotationMeta); } From aa379ae767b1f62a3d8e69520e8b8a0e856cd8dc Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sun, 4 Feb 2024 09:50:19 +0100 Subject: [PATCH 11/22] Fix: First annotation-layer not draggable --- apps/editor/src/components/editor/layers/layers.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index b119bb19a..4eded57bb 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -156,13 +156,8 @@ export const Layers: React.FC = observer(() => { const activeLayer = active.data.current?.layer as ILayer; // Return if we are not dragging a layer: if (!activeLayer) return; - // Return if we are dragging image layer within or above annotationGroup - const activeLayerIndex = layers?.indexOf(activeLayer) || 0; - if ( - !activeLayer.isAnnotation || - !layers?.[activeLayerIndex - 1]?.isAnnotation - ) - return; + // Return if we are dragging image layer + if (!activeLayer.isAnnotation) return; // Return if we are not dragging OVER a // group or if we are dragging over the layer's own group: const overGroup = over.data.current?.annotationGroup as IAnnotationGroup; @@ -182,7 +177,7 @@ export const Layers: React.FC = observer(() => { (overGroup as AnnotationGroup).addLayer(activeLayer); }); }, - [document, layers], + [document], ); // This handler is called when the user lets go of a layer or group after dragging: From 3d9643fcabe38559fb355ae9d5aff631be6c9d18 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Tue, 6 Feb 2024 02:13:07 +0100 Subject: [PATCH 12/22] Feat: Confirmation popups for delete annotation groups --- apps/editor/src/assets/de.json | 4 +- apps/editor/src/assets/en.json | 4 +- .../editor/layers/group-list-item.tsx | 102 ++++++++++++++---- .../editor/save-popup/save-popup.tsx | 5 + .../annotation-groups/annotation-group.ts | 13 ++- apps/editor/src/models/editor/document.ts | 5 +- libs/ui-shared/src/lib/types/editor/layers.ts | 4 + 7 files changed, 110 insertions(+), 27 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index 98a2d4775..c805998cc 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -437,7 +437,9 @@ "untitled-group": "Unbenannte Gruppe", "delete-annotation-group-message": "Wollen Sie die Annotation \"{{name}}\" und die folgenden Ebenen wirklich löschen??", - "delete-backend-data-warning": "Warnung: Durch das Löschen der Annotationsgruppe \"{{name}}\" werden auch bereits unter \"{{dataUri}}\" gespeicherte Daten unwiederruflich gelöscht!", + "warning": "Warnung:", + "delete-backend-data-warning": "Durch das Löschen der Annotationsgruppe \"{{name}}\" wird auch die unter \"{{dataUri}}\" gespeicherte Annotation unwiederruflich gelöscht!", + "unsaved-backend-annotations": "Folgende Gruppen sind noch nicht gespeichert!", "get-annotation-error": "Annotation konnte nicht gefunden werden", "get-annotation-error-description": "Die Annotation mit der ID \"{{id}}\" konnte nicht gefunden werden." } diff --git a/apps/editor/src/assets/en.json b/apps/editor/src/assets/en.json index 8a2c9f49f..5ce2f8dbc 100644 --- a/apps/editor/src/assets/en.json +++ b/apps/editor/src/assets/en.json @@ -438,7 +438,9 @@ "untitled-group": "Untitled group", "delete-annotation-group-message": "Do you really want to delete the annotation group \"{{name}}\" and following layers?", - "delete-backend-data-warning": "Warning: Deleting the annotation group \"{{name}}\" also irrevocably deletes data already saved under \"{{dataUri}}\"!", + "warning": "Warning:", + "delete-backend-data-warning": "Deleting the annotation group \"{{name}}\" also deletes the annotation saved under \"{{dataUri}}\"!", + "unsaved-backend-annotations": "Following annotation groups are not saved yet!", "get-annotation-error": "Annotation can not be found", "get-annotation-error-description": "Annotation with the ID \"{{id}}\" can not be found." } diff --git a/apps/editor/src/components/editor/layers/group-list-item.tsx b/apps/editor/src/components/editor/layers/group-list-item.tsx index 994c93459..3f96f1bb4 100644 --- a/apps/editor/src/components/editor/layers/group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/group-list-item.tsx @@ -9,6 +9,8 @@ import { IAnnotationGroup, ILayer, LayerList, + List, + ListItem, PointerButton, Text, useDoubleTap, @@ -36,6 +38,18 @@ const LayerListContainer = styled.div` padding: 16px; `; +const UnsavedGroupList = styled(List)` + margin: 10px 0; +`; + +const UnsavedGroupListItem = styled(ListItem)` + margin: -4px 0; +`; + +const StyledText = styled(Text)<{ space?: number }>` + margin-bottom: ${({ space }) => space || "2"}px; +`; + export const AnnotationGroupListItem = observer<{ group: IAnnotationGroup; isActive: boolean; @@ -163,8 +177,8 @@ export const AnnotationGroupListItem = observer<{ - - - - - {group.metadata && ( - - )} - + {store?.editor?.activeDocument?.annotationGroups + // eslint-disable-next-line @typescript-eslint/no-shadow + .filter((group) => group?.metadata?.id && group?.hasChanges)?.length === + 0 ? ( + + + + + {group.metadata && ( + + )} + + ) : ( + + + + + {group.metadata && ( + <> + + + + + {store?.editor?.activeDocument?.annotationGroups + // eslint-disable-next-line @typescript-eslint/no-shadow + .filter((group) => group?.metadata?.id && group?.hasChanges) + ?.map((groupToSave) => ( + + {`• ${groupToSave.title}`} + + ))} + + + )} + + )} ); }); diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index 75b051622..96c5b3761 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -167,6 +167,8 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { activeLayer?.getAnnotationGroupLayers().forEach((layer) => { store?.editor.activeDocument?.history?.updateCheckpoint(layer.id); }); + // Reset the layer count changes flag + activeLayer?.annotationGroup?.setHasLayerCountChange(false); return true; } catch (error) { if (error instanceof AxiosError) { @@ -244,6 +246,7 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion reviewTask.getAnnotation(newAnnotationId)! : annotationMeta; + // TODO: Refactor saving if (!annotationMeta) { addMetadataToGroup(activeLayer?.annotationGroup, newAnnotationMeta); } else { @@ -252,6 +255,8 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { activeLayer?.getAnnotationGroupLayers().forEach((layer) => { store?.editor.activeDocument?.history?.updateCheckpoint(layer.id); }); + // Reset the layer count changes flag + activeLayer?.annotationGroup?.setHasLayerCountChange(false); store?.setProgress(); return true; } catch (error) { diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index eee393dea..808a665af 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -30,6 +30,7 @@ export class AnnotationGroup public collapsed?: boolean; public metadata?: BackendMetadata; + public hasLayerCountChange = false; constructor( snapshot: Partial | undefined, @@ -40,12 +41,14 @@ export class AnnotationGroup makeObservable(this, { layerIds: observable, collapsed: observable, + hasLayerCountChange: observable, titleOverride: observable, isActive: computed, metadata: observable, setTitle: action, setCollapsed: action, + setHasLayerCountChange: action, setLayerIds: action, addLayer: action, removeLayer: action, @@ -58,10 +61,12 @@ export class AnnotationGroup } public get hasChanges() { - return this.layers.some( + const hasChangesInLayers = this.layers.some( (layer) => layer.kind === "image" && (layer as ImageLayer).hasChanges, ); + return hasChangesInLayers || this.hasLayerCountChange; } + public get title(): string | undefined { return this.titleOverride; } @@ -74,6 +79,10 @@ export class AnnotationGroup this.collapsed = value; } + public setHasLayerCountChange(value: boolean): void { + this.hasLayerCountChange = value; + } + public setLayerIds(ids: string[]) { this.layerIds = ids; } @@ -86,10 +95,12 @@ export class AnnotationGroup // we also remove it from there: this.document.removeLayerFromRootList(layer); }); + this.setHasLayerCountChange(true); } public removeLayer(layer: ILayer) { this.setLayerIds(this.layerIds.filter((layerId) => layerId !== layer.id)); + this.setHasLayerCountChange(true); } public get isActive() { diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index cd3585976..78ecab082 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -460,7 +460,10 @@ export class Document ); this.addLayer(annotationLayer); - const annotationGroup = new AnnotationGroup({ titleOverride: title }, this); + const annotationGroup = new AnnotationGroup( + { titleOverride: title || "untitled-group" }, + this, + ); this.addAnnotationGroup(annotationGroup); annotationGroup.addLayer(annotationLayer); diff --git a/libs/ui-shared/src/lib/types/editor/layers.ts b/libs/ui-shared/src/lib/types/editor/layers.ts index 347fc392e..e5609d07f 100644 --- a/libs/ui-shared/src/lib/types/editor/layers.ts +++ b/libs/ui-shared/src/lib/types/editor/layers.ts @@ -219,10 +219,14 @@ export interface IAnnotationGroup { collapsed?: boolean; /** Whether the group contains the document's active layer */ isActive: boolean; + /** Returns `true` if the annotation group has new layers or less layers and the changes are not saved yet. */ + hasLayerCountChange: boolean; /** Returns `true` if the annotation group has changes. */ hasChanges: boolean; /** Sets the group's title. */ setTitle(value?: string): void; + /** Sets the flag if the group experiences a change in the number of layers. */ + setHasLayerCountChange(value: boolean): void; /** Adds a layer to the group. Also removes it from the document root. */ addLayer(layer: ILayer): void; /** Removes the layer from the group, if it is part of the group. */ From d59c816011539fe154da754b36ff450bbb800bf5 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Fri, 9 Feb 2024 15:40:48 +0100 Subject: [PATCH 13/22] Fix: Review adjustments --- apps/editor/src/assets/de.json | 8 +- apps/editor/src/assets/en.json | 8 +- .../confirmation-popup.props.ts | 2 +- .../editor/layers/group-list-item.tsx | 98 +++++++------------ .../src/components/editor/layers/layers.tsx | 23 ++--- .../editor/save-popup/save-popup.tsx | 4 +- .../annotation-groups/annotation-group.ts | 16 +-- apps/editor/src/models/editor/document.ts | 8 +- libs/ui-shared/src/lib/types/editor/layers.ts | 4 +- 9 files changed, 71 insertions(+), 100 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index c805998cc..2437f2894 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -436,10 +436,12 @@ "review-description": "{{taskType}} der Annotationen des Bildes {{image}}.", "untitled-group": "Unbenannte Gruppe", - "delete-annotation-group-message": "Wollen Sie die Annotation \"{{name}}\" und die folgenden Ebenen wirklich löschen??", + "rename-group": "Gruppe umbenennen", + "delete-group": "Gruppe löschen", + "delete-annotation-group-message": "Wollen Sie die Annotation \"{{name}}\" und die folgenden Ebenen wirklich löschen?", "warning": "Warnung:", - "delete-backend-data-warning": "Durch das Löschen der Annotationsgruppe \"{{name}}\" wird auch die unter \"{{dataUri}}\" gespeicherte Annotation unwiederruflich gelöscht!", - "unsaved-backend-annotations": "Folgende Gruppen sind noch nicht gespeichert!", + "delete-backend-data-warning": "Durch das Löschen der Annotationsgruppe \"{{name}}\" wird auch die unter \"{{dataUri}}\" gespeicherte Annotation unwiderruflich gelöscht!", + "unsaved-backend-annotations": "Folgende Gruppen sind noch nicht gespeichert:", "get-annotation-error": "Annotation konnte nicht gefunden werden", "get-annotation-error-description": "Die Annotation mit der ID \"{{id}}\" konnte nicht gefunden werden." } diff --git a/apps/editor/src/assets/en.json b/apps/editor/src/assets/en.json index 5ce2f8dbc..9d6fab94f 100644 --- a/apps/editor/src/assets/en.json +++ b/apps/editor/src/assets/en.json @@ -437,10 +437,12 @@ "review-description": "{{taskType}} the annotations of {{image}}", "untitled-group": "Untitled group", + "rename-group": "Rename Group", + "delete-group": "Delete Group", "delete-annotation-group-message": "Do you really want to delete the annotation group \"{{name}}\" and following layers?", "warning": "Warning:", "delete-backend-data-warning": "Deleting the annotation group \"{{name}}\" also deletes the annotation saved under \"{{dataUri}}\"!", - "unsaved-backend-annotations": "Following annotation groups are not saved yet!", - "get-annotation-error": "Annotation can not be found", - "get-annotation-error-description": "Annotation with the ID \"{{id}}\" can not be found." + "unsaved-backend-annotations": "The following annotation groups are not saved yet:", + "get-annotation-error": "Annotation cannot be found", + "get-annotation-error-description": "Annotation with the ID \"{{id}}\" cannot be found." } diff --git a/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts b/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts index 3f929549b..1117614b0 100644 --- a/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts +++ b/apps/editor/src/components/data-manager/confirmation-popup/confirmation-popup.props.ts @@ -12,5 +12,5 @@ export interface ConfirmationPopUpProps cancel?: string; cancelTx?: string; onConfirm?: () => void; - children?: T; + children?: ReactNode; } diff --git a/apps/editor/src/components/editor/layers/group-list-item.tsx b/apps/editor/src/components/editor/layers/group-list-item.tsx index 3f96f1bb4..b7df6d61a 100644 --- a/apps/editor/src/components/editor/layers/group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/group-list-item.tsx @@ -111,7 +111,6 @@ export const AnnotationGroupListItem = observer<{ // Layer Renaming Handling const [isAnnotationGroupNameEditable, setIsAnnotationGroupNameEditable] = useState(false); - const startEditingAnnotationGroupName = useCallback( (event: React.PointerEvent) => { event.preventDefault(); @@ -177,8 +176,8 @@ export const AnnotationGroupListItem = observer<{ - {store?.editor?.activeDocument?.annotationGroups - // eslint-disable-next-line @typescript-eslint/no-shadow - .filter((group) => group?.metadata?.id && group?.hasChanges)?.length === - 0 ? ( - - - - - {group.metadata && ( + + + + + {group.metadata && ( + <> + - )} - - ) : ( - - - - - {group.metadata && ( - <> - - - - - {store?.editor?.activeDocument?.annotationGroups - // eslint-disable-next-line @typescript-eslint/no-shadow - .filter((group) => group?.metadata?.id && group?.hasChanges) - ?.map((groupToSave) => ( - - {`• ${groupToSave.title}`} - - ))} - - - )} - - )} + {store?.editor?.activeDocument?.annotationGroups.filter( + // eslint-disable-next-line @typescript-eslint/no-shadow + (group) => group?.metadata?.id && group?.hasChanges, + ).length !== 0 && ( + <> + + + {store?.editor?.activeDocument?.annotationGroups + // eslint-disable-next-line @typescript-eslint/no-shadow + .filter((group) => group?.metadata?.id && group?.hasChanges) + ?.map((groupToSave) => ( + + {`• ${groupToSave.title}`} + + ))} + + + )} + + )} + ); }); diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index ca040157d..005301f09 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -156,13 +156,8 @@ export const Layers: React.FC = observer(() => { const activeLayer = active.data.current?.layer as ILayer; // Return if we are not dragging a layer: if (!activeLayer) return; - // Return if we are dragging image layer within or above annotationGroup - const activeLayerIndex = layers?.indexOf(activeLayer) || 0; - if ( - !activeLayer.isAnnotation || - !layers?.[activeLayerIndex - 1]?.isAnnotation - ) - return; + // Return if we are dragging image layer + if (!activeLayer.isAnnotation) return; // Return if we are not dragging OVER a // group or if we are dragging over the layer's own group: const overGroup = over.data.current?.annotationGroup as IAnnotationGroup; @@ -182,7 +177,7 @@ export const Layers: React.FC = observer(() => { (overGroup as AnnotationGroup).addLayer(activeLayer); }); }, - [document, layers], + [document], ); // This handler is called when the user lets go of a layer or group after dragging: @@ -232,14 +227,12 @@ export const Layers: React.FC = observer(() => { newIndex, ); layer.annotationGroup.setLayerIds(newLayerIds); - } else if (!layer.annotationGroup && !layer.isAnnotation && layers) { + } + // Prevents dragging an image layer within or between respectivly above annotation groups. + else if (!layer.annotationGroup && !layer.isAnnotation && layers) { const oldIndex = layerIds.indexOf(layer.id); const newIndex = layerIds.indexOf(over.data?.current?.layer?.id); - const draggedImageLayer = layers.find( - (imageLayer) => imageLayer.id === layerIds[oldIndex], - ); - if (draggedImageLayer && newIndex !== -1) { - document.addLayer(draggedImageLayer, newIndex); + if (dragged?.layer && newIndex !== -1) { const newLayerIds = arrayMove(layerIds, oldIndex, newIndex); document.setLayerIds(newLayerIds); } @@ -337,7 +330,7 @@ export const Layers: React.FC = observer(() => { document?.imageLayers?.length >= (document?.maxVisibleLayers || 0) } - onPointerDown={() => document?.addNewAnnotationGroup()} + onPointerDown={document?.addNewAnnotationGroup} /> } diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index 96c5b3761..18055c1e7 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -168,7 +168,7 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { store?.editor.activeDocument?.history?.updateCheckpoint(layer.id); }); // Reset the layer count changes flag - activeLayer?.annotationGroup?.setHasLayerCountChange(false); + activeLayer?.annotationGroup?.setHasUnsavedChanges(false); return true; } catch (error) { if (error instanceof AxiosError) { @@ -256,7 +256,7 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { store?.editor.activeDocument?.history?.updateCheckpoint(layer.id); }); // Reset the layer count changes flag - activeLayer?.annotationGroup?.setHasLayerCountChange(false); + activeLayer?.annotationGroup?.setHasUnsavedChanges(false); store?.setProgress(); return true; } catch (error) { diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index 808a665af..52ab4ec18 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -30,7 +30,7 @@ export class AnnotationGroup public collapsed?: boolean; public metadata?: BackendMetadata; - public hasLayerCountChange = false; + protected hasUnsavedChanges = false; constructor( snapshot: Partial | undefined, @@ -41,14 +41,14 @@ export class AnnotationGroup makeObservable(this, { layerIds: observable, collapsed: observable, - hasLayerCountChange: observable, titleOverride: observable, isActive: computed, + hasChanges: computed, metadata: observable, setTitle: action, setCollapsed: action, - setHasLayerCountChange: action, + setHasUnsavedChanges: action, setLayerIds: action, addLayer: action, removeLayer: action, @@ -64,7 +64,7 @@ export class AnnotationGroup const hasChangesInLayers = this.layers.some( (layer) => layer.kind === "image" && (layer as ImageLayer).hasChanges, ); - return hasChangesInLayers || this.hasLayerCountChange; + return hasChangesInLayers || this.hasUnsavedChanges; } public get title(): string | undefined { @@ -79,8 +79,8 @@ export class AnnotationGroup this.collapsed = value; } - public setHasLayerCountChange(value: boolean): void { - this.hasLayerCountChange = value; + public setHasUnsavedChanges(value: boolean): void { + this.hasUnsavedChanges = value; } public setLayerIds(ids: string[]) { @@ -95,12 +95,12 @@ export class AnnotationGroup // we also remove it from there: this.document.removeLayerFromRootList(layer); }); - this.setHasLayerCountChange(true); + this.setHasUnsavedChanges(true); } public removeLayer(layer: ILayer) { this.setLayerIds(this.layerIds.filter((layerId) => layerId !== layer.id)); - this.setHasLayerCountChange(true); + this.setHasUnsavedChanges(true); } public get isActive() { diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index 78ecab082..cf6486a6e 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -449,7 +449,7 @@ export class Document : defaultRegionGrowingPreviewColor; }; - public addNewAnnotationGroup = (title?: string) => { + public addNewAnnotationGroup = () => { if (!this.mainImageLayer) return; const annotationColor = this.getFirstUnusedColor(); @@ -461,7 +461,7 @@ export class Document this.addLayer(annotationLayer); const annotationGroup = new AnnotationGroup( - { titleOverride: title || "untitled-group" }, + { titleOverride: annotationLayer.title }, this, ); this.addAnnotationGroup(annotationGroup); @@ -635,9 +635,7 @@ export class Document public finishBatchImport() { if (!this.layers.some((layer) => layer.isAnnotation)) { - this.addNewAnnotationGroup( - this.mainImageLayer?.title || "untitled-group", - ); + this.addNewAnnotationGroup(); this.viewport2D.setMainViewType(); } this.context?.persist(); diff --git a/libs/ui-shared/src/lib/types/editor/layers.ts b/libs/ui-shared/src/lib/types/editor/layers.ts index e5609d07f..43e600326 100644 --- a/libs/ui-shared/src/lib/types/editor/layers.ts +++ b/libs/ui-shared/src/lib/types/editor/layers.ts @@ -219,14 +219,12 @@ export interface IAnnotationGroup { collapsed?: boolean; /** Whether the group contains the document's active layer */ isActive: boolean; - /** Returns `true` if the annotation group has new layers or less layers and the changes are not saved yet. */ - hasLayerCountChange: boolean; /** Returns `true` if the annotation group has changes. */ hasChanges: boolean; /** Sets the group's title. */ setTitle(value?: string): void; /** Sets the flag if the group experiences a change in the number of layers. */ - setHasLayerCountChange(value: boolean): void; + setHasUnsavedChanges(value: boolean): void; /** Adds a layer to the group. Also removes it from the document root. */ addLayer(layer: ILayer): void; /** Removes the layer from the group, if it is part of the group. */ From 58be698418926ba3c505c50142894c2de8e51726 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sat, 10 Feb 2024 15:49:42 +0100 Subject: [PATCH 14/22] Refactor: SaveAs changes metadata for existing group --- .../editor/save-popup/save-popup.tsx | 73 +++---------------- apps/editor/src/models/editor/document.ts | 30 ++++---- apps/editor/src/models/editor/layers/layer.ts | 4 +- .../models/review-strategy/review-strategy.ts | 4 +- .../src/lib/types/editor/document.ts | 3 - 5 files changed, 31 insertions(+), 83 deletions(-) diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index 18055c1e7..257be6f51 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -91,7 +91,7 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { [newAnnotationURIPrefix, selectedExtension], ); - const addMetadataToGroup = ( + const changeMetaDataForGroup = ( annotationGroup: IAnnotationGroup | undefined, annotation: MiaAnnotation | undefined, ) => { @@ -188,36 +188,6 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { } }; - const loadOldAnnotation = async ( - newAnnotationMeta: MiaAnnotation, - oldAnnotationMeta: MiaAnnotation, - ) => { - const activeLayer = store?.editor.activeDocument?.activeLayer; - if (activeLayer?.annotationGroup) { - activeLayer.annotationGroup.metadata = { - ...newAnnotationMeta, - backend: "mia", - kind: "annotation", - }; - activeLayer.annotationGroup.title = newAnnotationMeta.dataUri; - activeLayer.annotationGroup.layers.forEach((layer, index) => { - layer.metadata = { - ...newAnnotationMeta, - backend: "mia", - kind: "annotation", - }; - layer.setTitle( - `${index + 1}_${newAnnotationMeta.dataUri.split("/").pop()}`, - ); - }); - } - const oldAnnotationFile = await fetchAnnotationFile(oldAnnotationMeta.id); - await importAnnotationFile(oldAnnotationFile); - if (activeLayer) { - store?.editor.activeDocument?.setActiveLayer(activeLayer.id); - } - }; - const saveAnnotationAs = async (uri: string) => { store?.setProgress({ labelTx: "saving" }); try { @@ -239,22 +209,11 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { const newAnnotationId = await reviewTask.createAnnotation([ annotationFile, ]); - const annotationMeta = activeLayer?.annotationGroup - ?.metadata as MiaAnnotation; - const newAnnotationMeta = - reviewTask instanceof MiaReviewTask - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - reviewTask.getAnnotation(newAnnotationId)! - : annotationMeta; - // TODO: Refactor saving - if (!annotationMeta) { - addMetadataToGroup(activeLayer?.annotationGroup, newAnnotationMeta); - } else { - await loadOldAnnotation(newAnnotationMeta, annotationMeta); + + if (reviewTask instanceof MiaReviewTask) { + const newAnnotation = await reviewTask.getAnnotation(newAnnotationId); + changeMetaDataForGroup(activeLayer?.annotationGroup, newAnnotation); } - activeLayer?.getAnnotationGroupLayers().forEach((layer) => { - store?.editor.activeDocument?.history?.updateCheckpoint(layer.id); - }); // Reset the layer count changes flag activeLayer?.annotationGroup?.setHasUnsavedChanges(false); store?.setProgress(); @@ -286,23 +245,11 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { const mainImageLayerMetadata = store?.editor.activeDocument?.mainImageLayer?.metadata; const imageName = getImageName(mainImageLayerMetadata); - const layerName = - store?.editor.activeDocument?.activeLayer?.title?.split(".")[0]; - const layerNameWithoutIndex = Number.isNaN( - +(layerName?.split("_")[0] ?? ""), - ) - ? layerName - : layerName?.split("_").slice(1).join("_"); - const layerNameWithIndexedName = Number.isNaN( - +(layerNameWithoutIndex?.split("_")[0] ?? ""), - ) - ? `1_${layerNameWithoutIndex}` - : layerNameWithoutIndex - ?.split("_") - .map((sub, index) => (index === 0 ? Number(sub) + 1 : sub)) - .join("_"); + const groupName = + store?.editor.activeDocument?.activeLayer?.annotationGroup?.title; + const groupNameWithUnderscores = groupName?.replace(/\s/g, "_"); return `/annotations/${imageName}/${ - layerNameWithIndexedName || "annotation" + groupNameWithUnderscores || "annotation" }`; }, [store]); @@ -322,7 +269,7 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { return pattern.test(dataUri) ? "valid" - : translate("data_uri_help_message"); + : translate("data-uri-help-message"); }, [translate], ); diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index cf6486a6e..deda45b71 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -345,13 +345,6 @@ export class Document return id ? this.layerMap[id] : undefined; } - public getOrphanAnnotationLayers(): ILayer[] { - const orphanAnnotationLayers = this.layers.filter( - (l) => l.isAnnotation && !l.annotationGroup, - ); - return orphanAnnotationLayers ?? []; - } - public getAnnotationGroup(id: string): IAnnotationGroup | undefined { return this.annotationGroupMap[id]; } @@ -585,7 +578,7 @@ export class Document layers: ILayer[], title?: string, ): Promise => { - const imageLayers = this.layers.filter( + const imageLayers = layers.filter( (potentialLayer) => potentialLayer instanceof ImageLayer && potentialLayer.isAnnotation, ) as ImageLayer[]; @@ -765,7 +758,7 @@ export class Document await this.importFiles( this.createAnnotationGroup( unzippedFiles, - filteredFiles.name, + filteredFiles.name.split(".")[0], this.getMetadataFromFile(filteredFiles), ), filteredFiles.name, @@ -789,15 +782,13 @@ export class Document // return; // } - if (!isAnnotation) { + if (!("annotationGroupId" in filteredFiles)) { if (filteredFiles instanceof File) { this.createAnnotationGroup( [filteredFiles], - filteredFiles.name, + filteredFiles.name.split(".")[0], this.getMetadataFromFile(filteredFiles), ); - } else { - this.createAnnotationGroup(filteredFiles, name ?? uuidv4()); } } let createdLayerId = ""; @@ -933,7 +924,20 @@ export class Document // }); // } // } else { + if (uniqueValues.size === 1 && uniqueValues.has(0)) { + createdLayerId = await this.importAnnotation( + { ...imageWithUnit, name: `${image.name}` }, + undefined, + false, + ); + if (files instanceof File) { + this.addLayerToAnnotationGroup(createdLayerId, files); + this.addMetadataToLayer(createdLayerId, files); + } + return; + } uniqueValues.forEach(async (value) => { + // Here is a bug when you save an empty image if (value === 0) return; createdLayerId = await this.importAnnotation( { ...imageWithUnit, name: `${value}_${image.name}` }, diff --git a/apps/editor/src/models/editor/layers/layer.ts b/apps/editor/src/models/editor/layers/layer.ts index f05cca26f..a8a3f5d34 100644 --- a/apps/editor/src/models/editor/layers/layer.ts +++ b/apps/editor/src/models/editor/layers/layer.ts @@ -110,9 +110,7 @@ export class Layer implements ILayer, ISerializable { } public getAnnotationGroupLayers(): ILayer[] { - return ( - this.annotationGroup?.layers ?? this.document.getOrphanAnnotationLayers() - ); + return this.annotationGroup?.layers ?? []; } public setBlendMode = (value?: BlendMode): void => { diff --git a/apps/editor/src/models/review-strategy/review-strategy.ts b/apps/editor/src/models/review-strategy/review-strategy.ts index 0e0862f1e..650c24c35 100644 --- a/apps/editor/src/models/review-strategy/review-strategy.ts +++ b/apps/editor/src/models/review-strategy/review-strategy.ts @@ -70,13 +70,15 @@ export abstract class ReviewStrategy { const groupFiles = this.store.editor.activeDocument?.createAnnotationGroup( annotationFiles, - `Annotation ${idx + 1}`, + annotationFiles[0].name, getMetadataFromChild ? { ...annotationFiles[0]?.metadata } : { id: annotationId, kind: "annotation", backend: "who" }, ); if (!groupFiles) throw new Error("No active Document"); + // isAnnotation is unnessesary because it's only called here + // with an array, and then the isAnnotation is not used in importFiles await this.store?.editor.activeDocument?.importFiles( groupFiles, undefined, diff --git a/libs/ui-shared/src/lib/types/editor/document.ts b/libs/ui-shared/src/lib/types/editor/document.ts index 029585369..71ac1477f 100644 --- a/libs/ui-shared/src/lib/types/editor/document.ts +++ b/libs/ui-shared/src/lib/types/editor/document.ts @@ -98,9 +98,6 @@ export interface IDocument { getAnnotationGroup(id: string): IAnnotationGroup | undefined; - /* returns all annotation layers that do not have an annotation group */ - getOrphanAnnotationLayers(): ILayer[]; - /** Sets the active layer. */ setActiveLayer(idOrLayer?: string | ILayer): void; From 079385b6cef54ecd068f1ca0f35bd83827a1702a Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Mon, 12 Feb 2024 12:30:14 +0100 Subject: [PATCH 15/22] Fix: Flanky import and imported group naming --- apps/editor/src/models/editor/document.ts | 64 +++++++++-------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index aef41e9c7..ac341515a 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -749,7 +749,7 @@ export class Document await this.importFiles( this.createAnnotationGroup( unzippedFiles, - filteredFiles.name, + path.basename(filteredFiles.name, path.extname(filteredFiles.name)), this.getMetadataFromFile(filteredFiles), ), filteredFiles.name, @@ -773,17 +773,6 @@ export class Document // return; // } - if (!isAnnotation) { - if (filteredFiles instanceof File) { - this.createAnnotationGroup( - [filteredFiles], - filteredFiles.name, - this.getMetadataFromFile(filteredFiles), - ); - } else { - this.createAnnotationGroup(filteredFiles, name ?? uuidv4()); - } - } let createdLayerId = ""; const isFirstLayer = !this.layerIds.length || !this.layers.some((l) => l.kind !== "group"); @@ -822,6 +811,17 @@ export class Document const imageWithUnit = { unit, ...image }; if (isAnnotation) { + // Creates an annotation group if it does not yet exists + if ( + !("annotationGroupId" in filteredFiles) && + filteredFiles instanceof File + ) { + this.createAnnotationGroup( + [filteredFiles], + path.basename(filteredFiles.name, path.extname(filteredFiles.name)), + this.getMetadataFromFile(filteredFiles), + ); + } createdLayerId = await this.importAnnotation(imageWithUnit); } else if (isAnnotation !== undefined) { createdLayerId = await this.importImage(imageWithUnit); @@ -883,10 +883,6 @@ export class Document name: `${layerIndex}_${imageWithUnit.name}`, ...prototypeImage, }); - if (files instanceof File) { - this.addLayerToAnnotationGroup(createdLayerId, files); - this.addMetadataToLayer(createdLayerId, files); - } } } else { this.setError({ @@ -894,8 +890,6 @@ export class Document descriptionTx: "image-loading-error", }); } - - this.removeEmptyGroups(); } else { //! TODO: #513 // const numberOfAnnotations = uniqueValues.size - 1; @@ -917,6 +911,18 @@ export class Document // }); // } // } else { + + // Creates an annotation group if it does not yet exists + if ( + !("annotationGroupId" in filteredFiles) && + filteredFiles instanceof File + ) { + this.createAnnotationGroup( + [filteredFiles], + path.basename(filteredFiles.name, path.extname(filteredFiles.name)), + this.getMetadataFromFile(filteredFiles), + ); + } uniqueValues.forEach(async (value) => { if (value === 0) return; createdLayerId = await this.importAnnotation( @@ -942,20 +948,6 @@ export class Document this.history.clear(); } - if (files instanceof File) { - this.addLayerToAnnotationGroup(createdLayerId, files); - this.addMetadataToLayer(createdLayerId, files); - } - - // Move all annotation groups with only image layers to the end of the list: - this.annotationGroups.forEach((group) => { - if (!group.layers.every((layer) => !layer.isAnnotation)) return; - this.addAnnotationGroup( - group as AnnotationGroup, - this.annotationGroups.length - 1, - ); - }); - return createdLayerId; } @@ -1225,12 +1217,4 @@ export class Document this.history.redo(this.activeLayer.id); } } - - protected removeEmptyGroups() { - Object.values(this.annotationGroupMap).forEach((group) => { - if (group.layers.length <= 1) { - this.removeAnnotationGroup(group.id); - } - }); - } } From 01750d4e0e843c3d8681cfae3d66c05178817055 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Mon, 12 Feb 2024 15:50:22 +0100 Subject: [PATCH 16/22] Fix: Updating hasChanges on annotation groups --- .../editor/layers/group-list-item.tsx | 6 ++--- .../editor/layers/layer-list-item.tsx | 1 + .../src/components/editor/layers/layers.tsx | 26 ++++++++++++++++--- .../editor/save-popup/save-popup.tsx | 2 +- .../annotation-groups/annotation-group.ts | 8 +++--- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/apps/editor/src/components/editor/layers/group-list-item.tsx b/apps/editor/src/components/editor/layers/group-list-item.tsx index b7df6d61a..fccc88d20 100644 --- a/apps/editor/src/components/editor/layers/group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/group-list-item.tsx @@ -241,15 +241,13 @@ export const AnnotationGroupListItem = observer<{ })} /> {store?.editor?.activeDocument?.annotationGroups.filter( - // eslint-disable-next-line @typescript-eslint/no-shadow - (group) => group?.metadata?.id && group?.hasChanges, + (g) => g.metadata?.id && g.hasChanges, ).length !== 0 && ( <> {store?.editor?.activeDocument?.annotationGroups - // eslint-disable-next-line @typescript-eslint/no-shadow - .filter((group) => group?.metadata?.id && group?.hasChanges) + .filter((g) => g.metadata?.id && g.hasChanges) ?.map((groupToSave) => ( {`• ${groupToSave.title}`} diff --git a/apps/editor/src/components/editor/layers/layer-list-item.tsx b/apps/editor/src/components/editor/layers/layer-list-item.tsx index 6dd06fae1..652e172c1 100644 --- a/apps/editor/src/components/editor/layers/layer-list-item.tsx +++ b/apps/editor/src/components/editor/layers/layer-list-item.tsx @@ -135,6 +135,7 @@ export const LayerListItem = observer<{ }, [layer, store]); const deleteLayer = useCallback(() => { + layer.annotationGroup?.setHasUnsavedChanges(true); if ( // eslint-disable-next-line no-alert window.confirm(t("delete-layer-confirmation", { layer: layer.title })) diff --git a/apps/editor/src/components/editor/layers/layers.tsx b/apps/editor/src/components/editor/layers/layers.tsx index 005301f09..fe5171d8a 100644 --- a/apps/editor/src/components/editor/layers/layers.tsx +++ b/apps/editor/src/components/editor/layers/layers.tsx @@ -129,6 +129,10 @@ export const Layers: React.FC = observer(() => { const [draggedLayer, setDraggedLayer] = useState(); const [draggedGroup, setDraggedGroup] = useState(); + const [ + draggedLayerPreviousAnnotationGroup, + setDraggedLayerPreviousAnnotationGroup, + ] = useState(); const dndSensors = useSensors( // Require the mouse to move before dragging so we capture normal clicks: @@ -141,6 +145,9 @@ export const Layers: React.FC = observer(() => { const dndDragStart = useCallback((event: DragStartEvent) => { if (event.active.data.current?.layer) { setDraggedLayer(event.active.data.current?.layer); + setDraggedLayerPreviousAnnotationGroup( + event.active.data.current.layer.annotationGroup, + ); } else if (event.active.data.current?.annotationGroup) { setDraggedGroup(event.active.data.current?.annotationGroup); } @@ -219,7 +226,7 @@ export const Layers: React.FC = observer(() => { ) { const oldIndex = layer.annotationGroup.layerIds.indexOf(layer.id); const newIndex = layer.annotationGroup.layerIds.indexOf( - over.data?.current?.layer.id, + over.data.current.layer.id, ); const newLayerIds = arrayMove( layer.annotationGroup.layerIds, @@ -227,6 +234,13 @@ export const Layers: React.FC = observer(() => { newIndex, ); layer.annotationGroup.setLayerIds(newLayerIds); + if ( + draggedLayerPreviousAnnotationGroup?.id !== + over.data.current.layer.annotationGroup.id + ) { + draggedLayerPreviousAnnotationGroup?.setHasUnsavedChanges(true); + over.data.current.layer.annotationGroup.setHasUnsavedChanges(true); + } } // Prevents dragging an image layer within or between respectivly above annotation groups. else if (!layer.annotationGroup && !layer.isAnnotation && layers) { @@ -240,8 +254,9 @@ export const Layers: React.FC = observer(() => { } setDraggedLayer(undefined); setDraggedGroup(undefined); + setDraggedLayerPreviousAnnotationGroup(undefined); }, - [document, layerIds, layers], + [document, draggedLayerPreviousAnnotationGroup, layerIds, layers], ); const listItems = document?.renderingOrder.map((element, index) => { @@ -286,6 +301,11 @@ export const Layers: React.FC = observer(() => { ); } + const handleAddLayer = useCallback(() => { + document?.addNewAnnotationLayer(); + document?.activeLayer?.annotationGroup?.setHasUnsavedChanges(true); + }, [document]); + return ( <> { document?.imageLayers?.length >= (document?.maxVisibleLayers || 0) } - onPointerDown={document?.addNewAnnotationLayer} + onPointerDown={() => handleAddLayer()} /> )} (({ isOpen, onClose }) => { activeLayer?.getAnnotationGroupLayers().forEach((layer) => { store?.editor.activeDocument?.history?.updateCheckpoint(layer.id); }); - // Reset the layer count changes flag + // Reset the layer unsaved changes flag activeLayer?.annotationGroup?.setHasUnsavedChanges(false); return true; } catch (error) { diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index 52ab4ec18..575967f14 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -38,10 +38,14 @@ export class AnnotationGroup ) { this.applySnapshot(snapshot); - makeObservable(this, { + makeObservable< + this, + "hasUnsavedChanges" | "titleOverride" | "layerIds" | "metadata" + >(this, { layerIds: observable, collapsed: observable, titleOverride: observable, + hasUnsavedChanges: observable, isActive: computed, hasChanges: computed, metadata: observable, @@ -95,12 +99,10 @@ export class AnnotationGroup // we also remove it from there: this.document.removeLayerFromRootList(layer); }); - this.setHasUnsavedChanges(true); } public removeLayer(layer: ILayer) { this.setLayerIds(this.layerIds.filter((layerId) => layerId !== layer.id)); - this.setHasUnsavedChanges(true); } public get isActive() { From 79d78776963377f38b4dae9ba846f758f6fe08ce Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Mon, 12 Feb 2024 22:29:32 +0100 Subject: [PATCH 17/22] Fix: Create squashed Nifti and export popup --- apps/editor/src/assets/de.json | 1 + apps/editor/src/assets/en.json | 1 + .../editor/export-popup/export-popup.tsx | 85 +++++++++++++++++-- .../editor/save-popup/save-popup.tsx | 10 --- apps/editor/src/event-handling/hotkeys.ts | 5 +- apps/editor/src/models/editor/document.ts | 26 +++--- 6 files changed, 94 insertions(+), 34 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index 2437f2894..a2647d8cb 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -125,6 +125,7 @@ "export-tooltip": "Export (Strg + E)", "exporting": "Exportieren", "layers-to-export": "Annotationsebenen, die exportiert werden sollen", + "should-export-images": "Bildebenden mit exportieren", "export-all-layers": "Alle Ebenen exportieren", "export-annotation-group": "Gruppe exportieren", "export-as": "Exportieren als", diff --git a/apps/editor/src/assets/en.json b/apps/editor/src/assets/en.json index 9d6fab94f..1b2065b72 100644 --- a/apps/editor/src/assets/en.json +++ b/apps/editor/src/assets/en.json @@ -125,6 +125,7 @@ "export-tooltip": "Export (Ctrl/Cmd + E)", "exporting": "Exporting", "layers-to-export": "Layers to export", + "should-export-images": "Include image layers", "export-all-layers": "Export all layers", "export-annotation-group": "Export annotation group", "export-as": "Export as", diff --git a/apps/editor/src/components/editor/export-popup/export-popup.tsx b/apps/editor/src/components/editor/export-popup/export-popup.tsx index 4dcec2ad9..2be9e7d63 100644 --- a/apps/editor/src/components/editor/export-popup/export-popup.tsx +++ b/apps/editor/src/components/editor/export-popup/export-popup.tsx @@ -2,6 +2,7 @@ import { Button, DropDown, ILayer, + InvisibleButton, LayerList, PopUp, Switch, @@ -47,6 +48,15 @@ const StyledDropDown = styled(DropDown)` background: none; `; +export const SelectionCheckbox = styled(InvisibleButton)<{ + emphasized?: boolean; +}>` + width: 18px; + margin-right: 8px; + opacity: ${({ emphasized }) => (emphasized ? 1 : 0.4)}; + transition: opacity 0.1s ease-in-out; +`; + export const ExportPopUp = observer(({ isOpen, onClose }) => { const store = useStore(); @@ -60,31 +70,47 @@ export const ExportPopUp = observer(({ isOpen, onClose }) => { const [selectedExtension, setSelectedExtension] = useState( fileExtensions[0].value, ); + const [shouldIncludeImages, setShouldIncludeImages] = useState(false); useEffect(() => { if (shouldExportAllLayers) { setLayersToExport( store?.editor?.activeDocument?.layers?.filter( - (layer) => layer.isAnnotation, + (layer) => layer.isAnnotation || shouldIncludeImages, ) ?? [], ); + } else if (shouldIncludeImages) { + const activeGroupLayers = + store?.editor?.activeDocument?.activeLayer?.getAnnotationGroupLayers(); + const imageLayers = + store?.editor?.activeDocument?.layers?.filter( + (layer) => !layer.isAnnotation, + ) ?? []; + + const combinedLayers = activeGroupLayers?.concat(imageLayers) ?? []; + setLayersToExport(combinedLayers); } else { setLayersToExport( - store?.editor?.activeDocument?.activeLayer - ?.getAnnotationGroupLayers() - .filter((layer) => layer.isAnnotation) ?? [], + store?.editor?.activeDocument?.activeLayer?.getAnnotationGroupLayers() ?? + [], ); } - }, [store, isOpen, shouldExportAllLayers]); + }, [store, isOpen, shouldExportAllLayers, shouldIncludeImages]); const handleExport = useCallback(async () => { store?.setProgress({ labelTx: "exporting" }); try { + const fileName = shouldExportAllLayers + ? undefined + : store?.editor?.activeDocument?.activeLayer?.annotationGroup?.title; if (selectedExtension === ".zip") { - await store?.editor.activeDocument?.exportZip(layersToExport, true); + await store?.editor.activeDocument?.exportZip(layersToExport, fileName); } else { - await store?.editor?.activeDocument?.exportSquashedNii(layersToExport); + await store?.editor?.activeDocument?.exportSquashedNii( + layersToExport, + fileName, + ); } } catch (error) { store?.setError({ @@ -94,7 +120,36 @@ export const ExportPopUp = observer(({ isOpen, onClose }) => { } finally { store?.setProgress(); } - }, [layersToExport, selectedExtension, store]); + }, [layersToExport, selectedExtension, shouldExportAllLayers, store]); + + const handleCheckIncludeImageLayer = useCallback( + (value: boolean) => { + if (value) { + const imageLayers = + store?.editor?.activeDocument?.layers?.filter( + (layer) => !layer.isAnnotation, + ) ?? []; + const newLayersToExport = layersToExport.concat(imageLayers); + setLayersToExport(newLayersToExport); + setShouldIncludeImages(true); + } else { + setLayersToExport(layersToExport.filter((layer) => layer.isAnnotation)); + setShouldIncludeImages(false); + } + }, + [layersToExport, store?.editor?.activeDocument?.layers], + ); + + const handleSelectExtension = useCallback((value: string) => { + if (value === ".nii.gz") { + setLayersToExport( + store?.editor?.activeDocument?.activeLayer?.getAnnotationGroupLayers() ?? + [], + ); + setShouldIncludeImages(false); + } + setSelectedExtension(value); + }, []); return ( (({ isOpen, onClose }) => { value={shouldExportAllLayers} onChange={setShouldExportAllLayers} /> + {selectedExtension === ".zip" && ( + + + + handleCheckIncludeImageLayer(!shouldIncludeImages) + } + emphasized={shouldIncludeImages} + /> + + )} setSelectedExtension(value)} + onChange={(value) => handleSelectExtension(value)} size="medium" borderRadius="default" /> diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index 257be6f51..8505ec6f6 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -23,9 +23,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { useStore } from "../../../app/root-store"; -import { importFilesToDocument } from "../../../import-handling"; import { MiaReviewTask } from "../../../models/review-strategy"; -import { fetchAnnotationFile } from "../../../queries"; import { SavePopUpProps } from "./save-popup.props"; const SectionLabel = styled(Text)` @@ -132,14 +130,6 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { } }; - const importAnnotationFile = async (annotationFile: FileWithMetadata) => { - const fileTransfer = new DataTransfer(); - fileTransfer.items.add(annotationFile); - if (store) { - await importFilesToDocument(fileTransfer.files, store); - } - }; - const canBeOverwritten = useCallback(() => { const activeLayer = store?.editor.activeDocument?.activeLayer; const annotation = activeLayer?.annotationGroup?.metadata as MiaAnnotation; diff --git a/apps/editor/src/event-handling/hotkeys.ts b/apps/editor/src/event-handling/hotkeys.ts index 297eb4c4a..5dea11846 100644 --- a/apps/editor/src/event-handling/hotkeys.ts +++ b/apps/editor/src/event-handling/hotkeys.ts @@ -364,7 +364,10 @@ export const generalHotkeys: IHotkey[] = [ action: (store) => { store.setProgress({ labelTx: "exporting" }); store.editor.activeDocument - ?.exportZip(store.editor.activeDocument.layers, true) + ?.exportZip( + store.editor.activeDocument.layers, + store.editor.activeDocument.activeLayer?.annotationGroup?.title, + ) .catch() .then(() => { store?.setProgress(); diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index deda45b71..aa2cd925b 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -544,20 +544,15 @@ export class Document // I/O // eslint-disable-next-line @typescript-eslint/no-shadow - public exportZip = async (layers: ILayer[], limitToAnnotations?: boolean) => { - const zip = await this.zipLayers( - layers.filter((layer) => !limitToAnnotations || layer.isAnnotation), - ); + public exportZip = async (layers: ILayer[], fileName?: string) => { + const zip = await this.zipLayers(layers); if (this.context?.getTracker()?.isActive) { const trackingFile = this.context.getTracker()?.toFile(); if (trackingFile) zip.setFile(trackingFile.name, trackingFile); } - FileSaver.saveAs( - await zip.toBlob(), - `${this.title?.split(".")[0] ?? "annotation"}.zip`, - ); + FileSaver.saveAs(await zip.toBlob(), `${fileName ?? "annotation"}.zip`); }; public createZip = async ( @@ -576,25 +571,28 @@ export class Document public createSquashedNii = async ( // eslint-disable-next-line @typescript-eslint/no-shadow layers: ILayer[], - title?: string, + fileName?: string, ): Promise => { const imageLayers = layers.filter( (potentialLayer) => potentialLayer instanceof ImageLayer && potentialLayer.isAnnotation, ) as ImageLayer[]; const file = await writeSingleMedicalImage( - imageLayers[imageLayers.length - 1].image.toITKImage( - imageLayers.slice(0, -1).map((layer) => layer.image), + imageLayers[0].image.toITKImage( + imageLayers.slice(1).map((layer) => layer.image), true, ), - `${title ?? this.title?.split(".")[0] ?? "annotaion"}.nii.gz`, + `${fileName ?? "annotaion"}.nii.gz`, ); return file; }; // eslint-disable-next-line @typescript-eslint/no-shadow - public exportSquashedNii = async (layers: ILayer[]) => { - const file: File | undefined = await this.createSquashedNii(layers); + public exportSquashedNii = async (layers: ILayer[], fileName?: string) => { + const file: File | undefined = await this.createSquashedNii( + layers, + fileName, + ); if (file) { const fileBlob = new Blob([file], { type: file.type }); FileSaver.saveAs( From 2c6a2c754ab4797ee3a97fb829d16a2178296147 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Fri, 16 Feb 2024 13:47:17 +0100 Subject: [PATCH 18/22] Fix: Fixes for presentation --- .../editor/export-popup/export-popup.tsx | 19 +++++++++++--- .../editor/save-popup/save-popup.tsx | 25 +++++++++++++------ .../models/review-strategy/review-strategy.ts | 8 +++--- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/apps/editor/src/components/editor/export-popup/export-popup.tsx b/apps/editor/src/components/editor/export-popup/export-popup.tsx index 2be9e7d63..e24b2f60a 100644 --- a/apps/editor/src/components/editor/export-popup/export-popup.tsx +++ b/apps/editor/src/components/editor/export-popup/export-popup.tsx @@ -9,6 +9,7 @@ import { Text, } from "@visian/ui-shared"; import { observer } from "mobx-react-lite"; +import path from "path"; import { useCallback, useEffect, useState } from "react"; import styled from "styled-components"; @@ -101,9 +102,21 @@ export const ExportPopUp = observer(({ isOpen, onClose }) => { store?.setProgress({ labelTx: "exporting" }); try { - const fileName = shouldExportAllLayers - ? undefined - : store?.editor?.activeDocument?.activeLayer?.annotationGroup?.title; + const annotationGroupTitle = + store?.editor?.activeDocument?.activeLayer?.annotationGroup?.title; + + let fileName; + + if (shouldExportAllLayers) { + fileName = undefined; + } else { + fileName = annotationGroupTitle + ? path.basename( + annotationGroupTitle, + path.extname(annotationGroupTitle), + ) + : undefined; + } if (selectedExtension === ".zip") { await store?.editor.activeDocument?.exportZip(layersToExport, fileName); } else { diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index 8505ec6f6..70d8878cf 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -91,17 +91,23 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { const changeMetaDataForGroup = ( annotationGroup: IAnnotationGroup | undefined, - annotation: MiaAnnotation | undefined, + annotationId: string, + uri: string, ) => { const document = store?.editor.activeDocument; - if (document && annotationGroup && annotation) { + if (document && annotationGroup && annotationId) { annotationGroup.metadata = { - ...annotation, + id: annotationId, backend: "mia", kind: "annotation", }; annotationGroup.layers.forEach((l) => { - l.metadata = { ...l, backend: "mia", kind: "annotation" }; + l.metadata = { + id: annotationId, + dataUri: uri, + backend: "mia", + kind: "annotation", + }; }); } return annotationGroup; @@ -199,10 +205,15 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { const newAnnotationId = await reviewTask.createAnnotation([ annotationFile, ]); - if (reviewTask instanceof MiaReviewTask) { - const newAnnotation = await reviewTask.getAnnotation(newAnnotationId); - changeMetaDataForGroup(activeLayer?.annotationGroup, newAnnotation); + changeMetaDataForGroup( + activeLayer?.annotationGroup, + newAnnotationId, + uri, + ); + activeLayer?.getAnnotationGroupLayers().forEach((layer) => { + store?.editor.activeDocument?.history?.updateCheckpoint(layer.id); + }); } // Reset the layer count changes flag activeLayer?.annotationGroup?.setHasUnsavedChanges(false); diff --git a/apps/editor/src/models/review-strategy/review-strategy.ts b/apps/editor/src/models/review-strategy/review-strategy.ts index 650c24c35..8259c1e6f 100644 --- a/apps/editor/src/models/review-strategy/review-strategy.ts +++ b/apps/editor/src/models/review-strategy/review-strategy.ts @@ -1,4 +1,5 @@ import { action, makeObservable, observable } from "mobx"; +import path from "path"; import { RootStore } from "../root"; import { ReviewStrategySnapshot } from "./review-strategy-snapshot"; @@ -70,15 +71,16 @@ export abstract class ReviewStrategy { const groupFiles = this.store.editor.activeDocument?.createAnnotationGroup( annotationFiles, - annotationFiles[0].name, + path.basename( + annotationFiles[0].name, + path.extname(annotationFiles[0].name), + ), getMetadataFromChild ? { ...annotationFiles[0]?.metadata } : { id: annotationId, kind: "annotation", backend: "who" }, ); if (!groupFiles) throw new Error("No active Document"); - // isAnnotation is unnessesary because it's only called here - // with an array, and then the isAnnotation is not used in importFiles await this.store?.editor.activeDocument?.importFiles( groupFiles, undefined, From 6a9566c018299b5c3213030bde4d8b7b061ff121 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sat, 24 Feb 2024 18:33:46 +0100 Subject: [PATCH 19/22] Fix: Merge request --- apps/editor/src/models/editor/document.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index 3b5b57934..bb9566371 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -936,6 +936,10 @@ export class Document undefined, false, ); + if (files instanceof File) { + this.addLayerToAnnotationGroup(createdLayerId, files); + this.addMetadataToLayer(createdLayerId, files); + } } uniqueValues.forEach(async (value) => { // Here is a bug when you save an empty image From 46f2f94cc8659c92f7d9102152364ca85dfd31c0 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Sat, 24 Feb 2024 18:44:44 +0100 Subject: [PATCH 20/22] Refactor: Remove popup for groups without metadata --- .../editor/layers/group-list-item.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/editor/src/components/editor/layers/group-list-item.tsx b/apps/editor/src/components/editor/layers/group-list-item.tsx index fccc88d20..f737c2830 100644 --- a/apps/editor/src/components/editor/layers/group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/group-list-item.tsx @@ -39,15 +39,16 @@ const LayerListContainer = styled.div` `; const UnsavedGroupList = styled(List)` - margin: 10px 0; + margin: 5px 0; `; const UnsavedGroupListItem = styled(ListItem)` margin: -4px 0; `; -const StyledText = styled(Text)<{ space?: number }>` - margin-bottom: ${({ space }) => space || "2"}px; +const StyledText = styled(Text)<{ marginBottom?: number; marginTop?: number }>` + margin-bottom: ${({ marginBottom }) => marginBottom || "2"}px; + margin-top: ${({ marginTop }) => marginTop || "2"}px; `; export const AnnotationGroupListItem = observer<{ @@ -134,6 +135,7 @@ export const AnnotationGroupListItem = observer<{ const [miaAnnotationToBeDeleted, setMiaAnnotationToBeDeleted] = useState(); + // Delete is not yet implemented for WHO const deleteAnnotationGroup = useCallback(async () => { if (group.metadata && isMiaAnnotationMetadata(group.metadata)) { try { @@ -148,11 +150,12 @@ export const AnnotationGroupListItem = observer<{ }), }); } - } else { - openDeleteConfirmationPopUp(); + } else if (!group.metadata) { + group.delete(); + setContextMenuPosition(null); } }, [ - group.metadata, + group, miaAnnotationToBeDeleted?.id, openDeleteConfirmationPopUp, store, @@ -244,7 +247,7 @@ export const AnnotationGroupListItem = observer<{ (g) => g.metadata?.id && g.hasChanges, ).length !== 0 && ( <> - + {store?.editor?.activeDocument?.annotationGroups .filter((g) => g.metadata?.id && g.hasChanges) From 70f3eaf4007ffaa6c8dfae1eaab26c373bccae19 Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Tue, 27 Feb 2024 17:11:59 +0100 Subject: [PATCH 21/22] Fix: File endings corrupt saving --- .../models/editor/annotation-groups/annotation-group.ts | 1 + apps/editor/src/models/editor/document.ts | 9 ++++++--- .../editor/src/models/review-strategy/review-strategy.ts | 6 ++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts index 87a951d8e..575967f14 100644 --- a/apps/editor/src/models/editor/annotation-groups/annotation-group.ts +++ b/apps/editor/src/models/editor/annotation-groups/annotation-group.ts @@ -18,6 +18,7 @@ import { import { v4 as uuidv4 } from "uuid"; import { Document } from "../document"; +import { ImageLayer } from "../layers"; export class AnnotationGroup implements IAnnotationGroup, ISerializable diff --git a/apps/editor/src/models/editor/document.ts b/apps/editor/src/models/editor/document.ts index ad12912b7..73a3f5e0c 100644 --- a/apps/editor/src/models/editor/document.ts +++ b/apps/editor/src/models/editor/document.ts @@ -771,10 +771,11 @@ export class Document }); await this.importFiles(newUnzippedFiles); } else { + const fileName = path.basename(filteredFiles.name); await this.importFiles( this.createAnnotationGroup( unzippedFiles, - path.basename(filteredFiles.name, path.extname(filteredFiles.name)), + fileName.slice(0, fileName.indexOf(".")), this.getMetadataFromFile(filteredFiles), ), filteredFiles.name, @@ -841,9 +842,10 @@ export class Document !("annotationGroupId" in filteredFiles) && filteredFiles instanceof File ) { + const fileName = path.basename(filteredFiles.name); this.createAnnotationGroup( [filteredFiles], - path.basename(filteredFiles.name, path.extname(filteredFiles.name)), + fileName.slice(0, fileName.indexOf(".")), this.getMetadataFromFile(filteredFiles), ); } @@ -942,9 +944,10 @@ export class Document !("annotationGroupId" in filteredFiles) && filteredFiles instanceof File ) { + const fileName = path.basename(filteredFiles.name); this.createAnnotationGroup( [filteredFiles], - path.basename(filteredFiles.name, path.extname(filteredFiles.name)), + fileName.slice(0, fileName.indexOf(".")), this.getMetadataFromFile(filteredFiles), ); } diff --git a/apps/editor/src/models/review-strategy/review-strategy.ts b/apps/editor/src/models/review-strategy/review-strategy.ts index dc3e33f20..e237abcc0 100644 --- a/apps/editor/src/models/review-strategy/review-strategy.ts +++ b/apps/editor/src/models/review-strategy/review-strategy.ts @@ -74,13 +74,11 @@ export abstract class ReviewStrategy { ); if (!annotationFiles) throw new Error("Annotation files not found"); + const fileName = path.basename(annotationFiles[0].name); const groupFiles = this.store.editor.activeDocument?.createAnnotationGroup( annotationFiles, - path.basename( - annotationFiles[0].name, - path.extname(annotationFiles[0].name), - ), + fileName.slice(0, fileName.indexOf(".")), getMetadataFromChild ? { ...annotationFiles[0]?.metadata } : { id: annotationId, kind: "annotation", backend: "who" }, From da4c03b8b60f9c360a742b6c7f78aaeb1609ba7c Mon Sep 17 00:00:00 2001 From: Tonybodo Date: Tue, 27 Feb 2024 18:31:02 +0100 Subject: [PATCH 22/22] Fix: Review changes --- apps/editor/src/assets/de.json | 2 +- .../layers/draggable-group-list-item.tsx | 28 +++++++++++++------ .../layers/draggable-layer-list-item.tsx | 28 +++++++++++++------ .../editor/save-popup/save-popup.tsx | 17 +++++++++-- apps/editor/src/models/editor/document.ts | 6 ++-- .../models/review-strategy/review-strategy.ts | 2 +- 6 files changed, 59 insertions(+), 24 deletions(-) diff --git a/apps/editor/src/assets/de.json b/apps/editor/src/assets/de.json index 2963f4e8e..04d073ce1 100644 --- a/apps/editor/src/assets/de.json +++ b/apps/editor/src/assets/de.json @@ -125,7 +125,7 @@ "export-tooltip": "Export (Strg + E)", "exporting": "Exportieren", "layers-to-export": "Annotationsebenen, die exportiert werden sollen", - "should-export-images": "Bildebenden mit exportieren", + "should-export-images": "Bildebenen mit exportieren", "export-all-layers": "Alle Ebenen exportieren", "export-annotation-group": "Gruppe exportieren", "export-as": "Exportieren als", diff --git a/apps/editor/src/components/editor/layers/draggable-group-list-item.tsx b/apps/editor/src/components/editor/layers/draggable-group-list-item.tsx index 4de1ac165..50d313d2d 100644 --- a/apps/editor/src/components/editor/layers/draggable-group-list-item.tsx +++ b/apps/editor/src/components/editor/layers/draggable-group-list-item.tsx @@ -2,9 +2,20 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { IAnnotationGroup, ILayer } from "@visian/ui-shared"; import { observer } from "mobx-react-lite"; +import styled from "styled-components"; import { AnnotationGroupListItem } from "./group-list-item"; +const DraggableDiv = styled.div<{ + opacity: number; + transform: string | undefined; + transition: string | undefined; +}>` + opacity: ${({ opacity }) => opacity}; + transform: ${({ transform }) => transform}; + transition: ${({ transition }) => transition}; +`; + export const DraggableAnnotationGroupListItem = observer<{ group: IAnnotationGroup; isActive: boolean; @@ -15,20 +26,21 @@ export const DraggableAnnotationGroupListItem = observer<{ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: group.id, data: { annotationGroup: group } }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragged ? 0.3 : 1, - }; - return ( -
+ -
+ ); }); diff --git a/apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx b/apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx index 0a537fbb9..5cf988590 100644 --- a/apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx +++ b/apps/editor/src/components/editor/layers/draggable-layer-list-item.tsx @@ -2,9 +2,20 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { ILayer } from "@visian/ui-shared"; import { observer } from "mobx-react-lite"; +import styled from "styled-components"; import { LayerListItem } from "./layer-list-item"; +const DraggableDiv = styled.div<{ + opacity: number; + transform: string | undefined; + transition: string | undefined; +}>` + opacity: ${({ opacity }) => opacity}; + transform: ${({ transform }) => transform}; + transition: ${({ transition }) => transition}; +`; + export const DraggableLayerListItem = observer<{ layer: ILayer; isActive?: boolean; @@ -17,15 +28,16 @@ export const DraggableLayerListItem = observer<{ data: { layer }, }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragged ? 0.3 : 1, - }; - return ( -
+ -
+ ); }); diff --git a/apps/editor/src/components/editor/save-popup/save-popup.tsx b/apps/editor/src/components/editor/save-popup/save-popup.tsx index b2cb2f912..20334d7d7 100644 --- a/apps/editor/src/components/editor/save-popup/save-popup.tsx +++ b/apps/editor/src/components/editor/save-popup/save-popup.tsx @@ -280,6 +280,17 @@ export const SavePopUp = observer(({ isOpen, onClose }) => { [newDataUri, isValidDataUri], ); + const handleRenameUri = useCallback( + (newUriPrefix: string) => { + setnewAnnotationURIPrefix(newUriPrefix); + const newGroupName = path.basename(newUriPrefix); + store?.editor?.activeDocument?.activeLayer?.annotationGroup?.setTitle( + newGroupName, + ); + }, + [store?.editor?.activeDocument?.activeLayer?.annotationGroup], + ); + return ( (({ isOpen, onClose }) => { )} - {isValidAnnotationUri !== "valid" && } + {isValidAnnotationUri !== "valid" && ( + + )} { - const zip = await this.zipLayers(layers); + public exportZip = async (layersToZip: ILayer[], fileName?: string) => { + const zip = await this.zipLayers(layersToZip); if (this.context?.getTracker()?.isActive) { const trackingFile = this.context.getTracker()?.toFile(); @@ -963,7 +962,6 @@ export class Document } } uniqueValues.forEach(async (value) => { - // Here is a bug when you save an empty image if (value === 0) return; createdLayerId = await this.importAnnotation( { ...imageWithUnit, name: `${value}_${image.name}` }, diff --git a/apps/editor/src/models/review-strategy/review-strategy.ts b/apps/editor/src/models/review-strategy/review-strategy.ts index e237abcc0..aa73095d9 100644 --- a/apps/editor/src/models/review-strategy/review-strategy.ts +++ b/apps/editor/src/models/review-strategy/review-strategy.ts @@ -68,7 +68,7 @@ export abstract class ReviewStrategy { ): Promise { if (!this.task?.annotationIds) return; await Promise.all( - this.task?.annotationIds.map(async (annotationId, idx) => { + this.task?.annotationIds.map(async (annotationId) => { const annotationFiles = await this.task?.getAnnotationFiles( annotationId, );