From c3729eaad8c3d0d1a66568dd58dd8e1d95d521ff Mon Sep 17 00:00:00 2001 From: frcroth Date: Tue, 3 Dec 2024 13:19:21 +0100 Subject: [PATCH 1/3] Warn user when using precomputed meshes after brushing (#8218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add key in volumetracing to store if volume bucket data has changed * show warning in context menu for loading precomputed meshes when bucket data has changed * avoid unnecessary rerenders in context menu * ensure SET_VOLUME_BUCKET_DATA_HAS_CHANGED is dispatched when buckets are changed * add missing file * fix linting * rename load_mesh_menu_item_label file * use action creator instead of hardcoding action * fix that volumeBucketDataHashChanged was not handled when the server sent it * add comments * update snapshots * add changelog entry * remove unintentional changelog entry * change action property from layerName to tracingId --------- Co-authored-by: Philipp Otto Co-authored-by: Philipp Otto Co-authored-by: Michael Büßemeyer --- app/models/annotation/AnnotationService.scala | 3 +- .../model/actions/volumetracing_actions.ts | 10 +++++ .../model/reducers/volumetracing_reducer.ts | 7 ++++ .../oxalis/model/sagas/quick_select_saga.ts | 8 ++-- .../oxalis/model/sagas/saga_helpers.ts | 34 +++++++++++++++- .../sagas/volume/volume_interpolation_saga.ts | 9 +++-- .../oxalis/model/sagas/volumetracing_saga.tsx | 15 +++---- frontend/javascripts/oxalis/store.ts | 1 + .../javascripts/oxalis/view/context_menu.tsx | 13 ++++-- .../load_mesh_menu_item_label.tsx | 37 ++++++++++++++++++ .../segments_tab/segment_list_item.tsx | 18 ++++----- .../volumetracing/volumetracing_saga.spec.ts | 9 +++-- .../annotations.e2e.js.md | 2 + .../annotations.e2e.js.snap | Bin 16824 -> 16858 bytes frontend/javascripts/types/api_flow_types.ts | 4 ++ .../proto/VolumeTracing.proto | 1 + .../volume/VolumeTracingService.scala | 4 +- 17 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/load_mesh_menu_item_label.tsx diff --git a/app/models/annotation/AnnotationService.scala b/app/models/annotation/AnnotationService.scala index 59233b08435..3b333d7bf78 100755 --- a/app/models/annotation/AnnotationService.scala +++ b/app/models/annotation/AnnotationService.scala @@ -179,7 +179,8 @@ class AnnotationService @Inject()( mappingName = mappingName, mags = magsRestricted.map(vec3IntToProto), hasSegmentIndex = Some(fallbackLayer.isEmpty || fallbackLayerHasSegmentIndex), - additionalAxes = AdditionalAxis.toProto(additionalAxes) + additionalAxes = AdditionalAxis.toProto(additionalAxes), + volumeBucketDataHasChanged = Some(false) ) } diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts index 781ac413557..caf749eeb3e 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.ts @@ -45,6 +45,9 @@ export type SetSegmentGroupsAction = ReturnType; export type SetExpandedSegmentGroupsAction = ReturnType; export type SetHasEditableMappingAction = ReturnType; export type SetMappingIsLockedAction = ReturnType; +export type SetVolumeBucketDataHasChangedAction = ReturnType< + typeof setVolumeBucketDataHasChangedAction +>; export type ComputeQuickSelectForRectAction = ReturnType; export type ComputeQuickSelectForPointAction = ReturnType; @@ -99,6 +102,7 @@ export type VolumeTracingAction = | FineTuneQuickSelectAction | CancelQuickSelectAction | ConfirmQuickSelectAction + | SetVolumeBucketDataHasChangedAction | BatchUpdateGroupsAndSegmentsAction; export const VolumeTracingSaveRelevantActions = [ @@ -437,3 +441,9 @@ export const batchUpdateGroupsAndSegmentsAction = (actions: BatchableUpdateSegme export const cancelQuickSelectAction = () => ({ type: "CANCEL_QUICK_SELECT" }) as const; export const confirmQuickSelectAction = () => ({ type: "CONFIRM_QUICK_SELECT" }) as const; + +export const setVolumeBucketDataHasChangedAction = (tracingId: string) => + ({ + type: "SET_VOLUME_BUCKET_DATA_HAS_CHANGED", + tracingId, + }) as const; diff --git a/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts b/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts index e7ffa4f8da8..ec4ea3835af 100644 --- a/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer.ts @@ -269,6 +269,7 @@ export function serverVolumeToClientVolumeTracing(tracing: ServerVolumeTracing): mappingName: tracing.mappingName, hasEditableMapping: tracing.hasEditableMapping, mappingIsLocked: tracing.mappingIsLocked, + volumeBucketDataHasChanged: tracing.volumeBucketDataHasChanged, hasSegmentIndex: tracing.hasSegmentIndex || false, additionalAxes: convertServerAdditionalAxesToFrontEnd(tracing.additionalAxes), }; @@ -377,6 +378,12 @@ function VolumeTracingReducer( return expandSegmentParents(state, action); } + case "SET_VOLUME_BUCKET_DATA_HAS_CHANGED": { + return updateVolumeTracing(state, action.tracingId, { + volumeBucketDataHasChanged: true, + }); + } + default: // pass } diff --git a/frontend/javascripts/oxalis/model/sagas/quick_select_saga.ts b/frontend/javascripts/oxalis/model/sagas/quick_select_saga.ts index d863505aed4..791d51e9211 100644 --- a/frontend/javascripts/oxalis/model/sagas/quick_select_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/quick_select_saga.ts @@ -15,7 +15,7 @@ import performQuickSelectML from "./quick_select_ml_saga"; import getSceneController from "oxalis/controller/scene_controller_provider"; import { getActiveSegmentationTracing } from "../accessors/volumetracing_accessor"; import type { VolumeTracing } from "oxalis/store"; -import { ensureMaybeActiveMappingIsLocked } from "./saga_helpers"; +import { requestBucketModificationInVolumeTracing } from "./saga_helpers"; function* shouldUseHeuristic() { const useHeuristic = yield* select((state) => state.userConfiguration.quickSelect.useHeuristic); @@ -32,11 +32,11 @@ export default function* listenToQuickSelect(): Saga { ); if (volumeTracing) { // As changes to the volume layer will be applied, the potentially existing mapping should be locked to ensure a consistent state. - const { isMappingLockedIfNeeded } = yield* call( - ensureMaybeActiveMappingIsLocked, + const isModificationAllowed = yield* call( + requestBucketModificationInVolumeTracing, volumeTracing, ); - if (!isMappingLockedIfNeeded) { + if (!isModificationAllowed) { return; } } diff --git a/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts b/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts index 2ebc695b159..abf7b049b1e 100644 --- a/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts +++ b/frontend/javascripts/oxalis/model/sagas/saga_helpers.ts @@ -9,7 +9,10 @@ import { call, put, takeEvery } from "typed-redux-saga"; import Toast from "libs/toast"; import { Store } from "oxalis/singletons"; import type { ActionPattern } from "@redux-saga/types"; -import { setMappingIsLockedAction } from "../actions/volumetracing_actions"; +import { + setMappingIsLockedAction, + setVolumeBucketDataHasChangedAction, +} from "../actions/volumetracing_actions"; import { MappingStatusEnum } from "oxalis/constants"; export function* takeEveryUnlessBusy

( @@ -113,4 +116,33 @@ export function* ensureMaybeActiveMappingIsLocked( } } +export function* requestBucketModificationInVolumeTracing( + volumeTracing: VolumeTracing, +): Saga { + /* + * Should be called when the modification of bucket data is about to happen. If + * the saga returns false, the modification should be cancelled (this happens if + * the user is not okay with locking the mapping). + * + * In detail: When the bucket data of a volume tracing is supposed to be mutated, we need to do + * two things: + * 1) ensure that the current mapping (or no mapping) is locked so that the mapping is not + * changed later (this would lead to inconsistent data). If the mapping state is not yet + * locked, the user is asked whether it is okay to lock it. + * If the user confirms this, the mapping is locked and we can continue with (2). If the + * user denies the locking request, the original bucket mutation will NOT be executed. + * 2) volumeTracing.volumeBucketDataHasChanged will bet set to true if the user didn't + * deny the request for locking the mapping. + */ + + const { isMappingLockedIfNeeded } = yield* call(ensureMaybeActiveMappingIsLocked, volumeTracing); + if (!isMappingLockedIfNeeded) { + return false; + } + + // Mark that bucket data has changed + yield* put(setVolumeBucketDataHasChangedAction(volumeTracing.tracingId)); + return true; +} + export default {}; diff --git a/frontend/javascripts/oxalis/model/sagas/volume/volume_interpolation_saga.ts b/frontend/javascripts/oxalis/model/sagas/volume/volume_interpolation_saga.ts index 55f47e57724..3d1690b9363 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/volume_interpolation_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/volume/volume_interpolation_saga.ts @@ -38,7 +38,7 @@ import { Model, api } from "oxalis/singletons"; import type { OxalisState } from "oxalis/store"; import { call, put } from "typed-redux-saga"; import { createVolumeLayer, getBoundingBoxForViewport, labelWithVoxelBuffer2D } from "./helpers"; -import { ensureMaybeActiveMappingIsLocked } from "../saga_helpers"; +import { requestBucketModificationInVolumeTracing } from "../saga_helpers"; /* * This saga is capable of doing segment interpolation between two slices. @@ -436,8 +436,11 @@ export default function* maybeInterpolateSegmentationLayer(): Saga { return; } // As the interpolation will be applied, the potentially existing mapping should be locked to ensure a consistent state. - const { isMappingLockedIfNeeded } = yield* call(ensureMaybeActiveMappingIsLocked, volumeTracing); - if (!isMappingLockedIfNeeded) { + const isModificationAllowed = yield* call( + requestBucketModificationInVolumeTracing, + volumeTracing, + ); + if (!isModificationAllowed) { return; } diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index f1770e8598c..12e1919ac31 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -82,7 +82,7 @@ import { select, take } from "oxalis/model/sagas/effect-generators"; import listenToMinCut from "oxalis/model/sagas/min_cut_saga"; import listenToQuickSelect from "oxalis/model/sagas/quick_select_saga"; import { - ensureMaybeActiveMappingIsLocked, + requestBucketModificationInVolumeTracing, takeEveryUnlessBusy, } from "oxalis/model/sagas/saga_helpers"; import { @@ -223,11 +223,11 @@ export function* editVolumeLayerAsync(): Saga { const activeCellId = yield* select((state) => enforceActiveVolumeTracing(state).activeCellId); // As changes to the volume layer will be applied, the potentially existing mapping should be locked to ensure a consistent state. - const { isMappingLockedIfNeeded } = yield* call( - ensureMaybeActiveMappingIsLocked, + const isModificationAllowed = yield* call( + requestBucketModificationInVolumeTracing, volumeTracing, ); - if (!isMappingLockedIfNeeded) { + if (!isModificationAllowed) { continue; } @@ -453,11 +453,11 @@ export function* floodFill(): Saga { } // As the flood fill will be applied to the volume layer, // the potentially existing mapping should be locked to ensure a consistent state. - const { isMappingLockedIfNeeded } = yield* call( - ensureMaybeActiveMappingIsLocked, + const isModificationAllowed = yield* call( + requestBucketModificationInVolumeTracing, volumeTracing, ); - if (!isMappingLockedIfNeeded) { + if (!isModificationAllowed) { continue; } yield* put(setBusyBlockingInfoAction(true, "Floodfill is being computed.")); @@ -603,6 +603,7 @@ export function* finishLayer( yield* put(registerLabelPointAction(layer.getUnzoomedCentroid())); } + export function* ensureToolIsAllowedInMag(): Saga { yield* take("INITIALIZE_VOLUMETRACING"); diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index deca802926a..26fb97c0dd0 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -264,6 +264,7 @@ export type VolumeTracing = TracingBase & { readonly hasEditableMapping?: boolean; readonly mappingIsLocked?: boolean; readonly hasSegmentIndex: boolean; + readonly volumeBucketDataHasChanged?: boolean; }; export type ReadOnlyTracing = TracingBase & { readonly type: "readonly"; diff --git a/frontend/javascripts/oxalis/view/context_menu.tsx b/frontend/javascripts/oxalis/view/context_menu.tsx index 21ddce3d0ca..634de6a7c59 100644 --- a/frontend/javascripts/oxalis/view/context_menu.tsx +++ b/frontend/javascripts/oxalis/view/context_menu.tsx @@ -131,6 +131,7 @@ import { import { hideContextMenuAction, setActiveUserBoundingBoxId } from "oxalis/model/actions/ui_actions"; import { getDisabledInfoForTools } from "oxalis/model/accessors/tool_accessor"; import FastTooltip from "components/fast_tooltip"; +import { LoadMeshMenuItemLabel } from "./right-border-tabs/segments_tab/load_mesh_menu_item_label"; type ContextMenuContextValue = React.MutableRefObject | null; export const ContextMenuContext = createContext(null); @@ -1179,7 +1180,9 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] segmentationLayerName, mappingInfo, ), - label: "Load Mesh (precomputed)", + label: ( + + ), }; const computeMeshAdHocItem = { key: "compute-mesh-adhc", @@ -1434,12 +1437,13 @@ function ContextMenuInner(propsWithInputRef: Props) { maybeClickedMeshId != null ? maybeClickedMeshId : segmentIdAtPosition; const wasSegmentOrMeshClicked = clickedSegmentOrMeshId > 0; - const { dataset, tracing, flycam } = useSelector((state: OxalisState) => state); + const dataset = useSelector((state: OxalisState) => state.dataset); useEffect(() => { Store.dispatch(ensureSegmentIndexIsLoadedAction(visibleSegmentationLayer?.name)); }, [visibleSegmentationLayer]); - const isSegmentIndexAvailable = useSelector((state: OxalisState) => - getMaybeSegmentIndexAvailability(state.dataset, visibleSegmentationLayer?.name), + const isSegmentIndexAvailable = getMaybeSegmentIndexAvailability( + dataset, + visibleSegmentationLayer?.name, ); const mappingName: string | null | undefined = useSelector((state: OxalisState) => { if (volumeTracing?.mappingName != null) return volumeTracing?.mappingName; @@ -1453,6 +1457,7 @@ function ContextMenuInner(propsWithInputRef: Props) { const isLoadingVolumeAndBB = [isLoadingMessage, isLoadingMessage]; const [segmentVolumeLabel, boundingBoxInfoLabel] = useFetch( async () => { + const { tracing, flycam } = Store.getState(); // The value that is returned if the context menu is closed is shown if it's still loading if (contextMenuPosition == null || !wasSegmentOrMeshClicked) return isLoadingVolumeAndBB; if (visibleSegmentationLayer == null || !isSegmentIndexAvailable) return []; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/load_mesh_menu_item_label.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/load_mesh_menu_item_label.tsx new file mode 100644 index 00000000000..0faa65c529f --- /dev/null +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/load_mesh_menu_item_label.tsx @@ -0,0 +1,37 @@ +import { WarningOutlined } from "@ant-design/icons"; +import FastTooltip from "components/fast_tooltip"; +import type { VolumeTracing } from "oxalis/store"; +import type { APIMeshFile } from "types/api_flow_types"; + +type Props = { + currentMeshFile: APIMeshFile | null | undefined; + volumeTracing: VolumeTracing | null | undefined; +}; + +export function LoadMeshMenuItemLabel({ currentMeshFile, volumeTracing }: Props) { + const showWarning = + volumeTracing?.volumeBucketDataHasChanged ?? + // For older annotations, volumeBucketDataHasChanged can be undefined. + // In that case, we still want to show a warning if no proofreading was + // done, but the mapping is still locked (i.e., the user brushed). + (!volumeTracing?.hasEditableMapping && volumeTracing?.mappingIsLocked); + + return ( + + + Load Mesh (precomputed) + + {showWarning && ( + + + + )} + + ); +} diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx index dd4ea4ac386..04ba5ef6737 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segment_list_item.tsx @@ -40,6 +40,7 @@ import { V4 } from "libs/mjs"; import { ChangeColorMenuItemContent } from "components/color_picker"; import type { MenuItemType } from "antd/es/menu/interface"; import { withMappingActivationConfirmation } from "./segments_view_helper"; +import { LoadMeshMenuItemLabel } from "./load_mesh_menu_item_label"; import type { AdditionalCoordinate } from "types/api_flow_types"; import { getAdditionalCoordinatesAsString, @@ -80,8 +81,10 @@ const getLoadPrecomputedMeshMenuItem = ( hideContextMenu: (_ignore?: any) => void, layerName: string | null | undefined, mappingInfo: ActiveMappingInfo, + activeVolumeTracing: VolumeTracing | null | undefined, ) => { const mappingName = currentMeshFile != null ? currentMeshFile.mappingName : undefined; + return { key: "loadPrecomputedMesh", disabled: !currentMeshFile, @@ -114,16 +117,10 @@ const getLoadPrecomputedMeshMenuItem = ( mappingInfo, ), label: ( - - Load Mesh (precomputed) - + ), }; }; @@ -429,6 +426,7 @@ function _SegmentListItem({ hideContextMenu, visibleSegmentationLayer != null ? visibleSegmentationLayer.name : null, mappingInfo, + activeVolumeTracing, ), getComputeMeshAdHocMenuItem( segment, diff --git a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts index 34bfd2c302b..206b45c64d3 100644 --- a/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts +++ b/frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts @@ -40,7 +40,10 @@ mockRequire("libs/toast", { const { setupSavingForTracingType } = require("oxalis/model/sagas/save_saga"); const { editVolumeLayerAsync, finishLayer } = require("oxalis/model/sagas/volumetracing_saga"); -const { ensureMaybeActiveMappingIsLocked } = require("oxalis/model/sagas/saga_helpers"); +const { + requestBucketModificationInVolumeTracing, + ensureMaybeActiveMappingIsLocked, +} = require("oxalis/model/sagas/saga_helpers"); const VolumeLayer = require("oxalis/model/volumetracing/volumelayer").default; @@ -437,11 +440,11 @@ test("VolumeTracingSaga should lock an active mapping upon first volume annotati mag: [1, 1, 1], zoomStep: 0, }); - // Test whether nested saga ensureMaybeActiveMappingIsLocked is called. + // Test whether nested saga requestBucketModificationInVolumeTracing is called. expectValueDeepEqual( t, saga.next(ACTIVE_CELL_ID), - call(ensureMaybeActiveMappingIsLocked, volumeTracing), + call(requestBucketModificationInVolumeTracing, volumeTracing), ); }); diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md index 222a9ed027d..48087c9ed20 100644 --- a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md @@ -1318,6 +1318,7 @@ Generated by [AVA](https://avajs.dev). typ: 'Volume', userBoundingBoxes: [], version: 0, + volumeBucketDataHasChanged: false, zoomLevel: 1, } @@ -1408,6 +1409,7 @@ Generated by [AVA](https://avajs.dev). typ: 'Volume', userBoundingBoxes: [], version: 0, + volumeBucketDataHasChanged: false, zoomLevel: 1, }, } diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.snap b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.snap index 818cad4f78548902018823349f8c01c8015e3f9b..8a0b4fd925d493beaebeb156e2226ecc6b5a7516 100644 GIT binary patch literal 16858 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 b|IY5~2>LpLzK)>xbp-zhO9m>j_r(DK2^6=Z literal 16824 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 diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index e3fb3502ed8..958651b32ec 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -875,6 +875,10 @@ export type ServerVolumeTracing = ServerTracingBase & { hasEditableMapping?: boolean; mappingIsLocked?: boolean; hasSegmentIndex?: boolean; + // volumeBucketDataHasChanged is automatically set to true by the back-end + // once a bucket was mutated. There is no need to send an explicit UpdateAction + // for that. + volumeBucketDataHasChanged?: boolean; }; export type ServerTracing = ServerSkeletonTracing | ServerVolumeTracing; export type ServerEditableMapping = { diff --git a/webknossos-datastore/proto/VolumeTracing.proto b/webknossos-datastore/proto/VolumeTracing.proto index 0f06affd9ad..495d1a481ff 100644 --- a/webknossos-datastore/proto/VolumeTracing.proto +++ b/webknossos-datastore/proto/VolumeTracing.proto @@ -48,6 +48,7 @@ message VolumeTracing { repeated AdditionalCoordinateProto editPositionAdditionalCoordinates = 21; repeated AdditionalAxisProto additionalAxes = 22; // Additional axes for which this tracing is defined optional bool mappingIsLocked = 23; // user may not select another mapping (e.g. because they have already mutated buckets) + optional bool volumeBucketDataHasChanged = 24; // The volume bucket data has been edited at least once } message SegmentGroup { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala index 5dfe5bb6723..f78c53cd0b8 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala @@ -210,7 +210,7 @@ class VolumeTracingService @Inject()( ) ?~> "failed to update segment index" } yield () } - } yield volumeTracing + } yield volumeTracing.copy(volumeBucketDataHasChanged = Some(true)) override def editableMappingTracingId(tracing: VolumeTracing, tracingId: String): Option[String] = if (tracing.getHasEditableMapping) Some(tracingId) else None @@ -281,7 +281,7 @@ class VolumeTracingService @Inject()( } yield () })) _ <- segmentIndexBuffer.flush() - } yield volumeTracing + } yield volumeTracing.copy(volumeBucketDataHasChanged = Some(true)) private def assertMagIsValid(tracing: VolumeTracing, mag: Vec3Int): Fox[Unit] = if (tracing.mags.nonEmpty) { From 2781061580a6a21c435a4f6213f493ac109d62e4 Mon Sep 17 00:00:00 2001 From: robert-oleynik <62473688+robert-oleynik@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:01:47 +0100 Subject: [PATCH 2/3] Fix `FATAL: role "postgres" does not exists` for docker compose deployment (#8240) * update pg_isready args to database configuration * update changelog --- CHANGELOG.unreleased.md | 1 + docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 8a3c9a0f3d1..752cda32872 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -36,6 +36,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Fixed a bug where trying to delete a non-existing node (via the API, for example) would delete the whole active tree. [#8176](https://github.com/scalableminds/webknossos/pull/8176) - Fixed a bug where dataset uploads would fail if the organization directory on disk is missing. [#8230](https://github.com/scalableminds/webknossos/pull/8230) - Fixed some layout issues in the upload view. [#8231](https://github.com/scalableminds/webknossos/pull/8231) +- Fixed `FATAL: role "postgres" does not exist` error message in Docker compose. [#8240](https://github.com/scalableminds/webknossos/pull/8240) ### Removed - Removed support for HTTP API versions 3 and 4. [#8075](https://github.com/scalableminds/webknossos/pull/8075) diff --git a/docker-compose.yml b/docker-compose.yml index a1f0e549c0e..879f777e4f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -222,7 +222,7 @@ services: POSTGRES_USER: webknossos_user POSTGRES_PASSWORD: secret_password healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -h 127.0.0.1 -p 5432"] + test: ["CMD-SHELL", "pg_isready -d webknossos -U webknossos_user -h 127.0.0.1 -p 5432"] interval: 2s timeout: 5s retries: 30 From 9a5c8e82b10dc9e4738d669bf7a33da0b5dc04f4 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 4 Dec 2024 09:35:42 +0100 Subject: [PATCH 3/3] Fix image in docs --- CHANGELOG.released.md | 2 +- docs/users/new_users.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.released.md b/CHANGELOG.released.md index 0dd98179efd..85470e55ef7 100644 --- a/CHANGELOG.released.md +++ b/CHANGELOG.released.md @@ -451,7 +451,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Fixed - Fixed that some segment (group) actions were not properly disabled for non-editable segmentation layers. [#7207](https://github.com/scalableminds/webknossos/issues/7207) - Fixed a bug where data from zarr2 datasets that have a channel axis was broken. [#7374](https://github.com/scalableminds/webknossos/pull/7374) -- Fixed a bug which changed the cursor position while editing the name of a tree or the comment of a node. [#7390](#https://github.com/scalableminds/webknossos/pull/7390) +- Fixed a bug which changed the cursor position while editing the name of a tree or the comment of a node. [#7390](https://github.com/scalableminds/webknossos/pull/7390) - Streaming sharded zarr3 datasets from servers which do not respond with Accept-Ranges header is now possible. [#7392](https://github.com/scalableminds/webknossos/pull/7392) ## [23.10.2](https://github.com/scalableminds/webknossos/releases/tag/23.10.2) - 2023-09-26 diff --git a/docs/users/new_users.md b/docs/users/new_users.md index a9d38485761..23d5dba6381 100644 --- a/docs/users/new_users.md +++ b/docs/users/new_users.md @@ -3,7 +3,7 @@ On webknossos.org, users can either sign up for a WEBKNOSSOS account by themselv As an admin or team manager, you can invite users to join your WEBKNOSSOS organization by clicking the `Invite Users` button at the top of the `Users` list available from the `Admin` menu in the navbar. This will open a popup where you can enter a list of email addresses, which will receive a custom invitation link. Users that click on this link are automatically assigned to your organization, and will not need manual activation. - ![Send an invite link to new users](../images/users_invite.jpeg) +![Send an invite link to new users](../images/users_invite.jpeg) ## Experience Levels For a fine-grained assignment to [annotation tasks](../tasks_projects/tasks.md), each user can have one or more experience levels assigned to them. Based on their respective experience level, tasks may or may not be distributed to them.