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,
);