diff --git a/frontend/javascripts/dashboard/dashboard_view.tsx b/frontend/javascripts/dashboard/dashboard_view.tsx index b1006e77214..945566a8e1f 100644 --- a/frontend/javascripts/dashboard/dashboard_view.tsx +++ b/frontend/javascripts/dashboard/dashboard_view.tsx @@ -278,7 +278,7 @@ class DashboardView extends PureComponent { ) : null; return ( - + {whatsNextBanner}
{pricingPlanWarnings} diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 6c09b28fc88..26d2e3840bc 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -60,11 +60,14 @@ import messages from "messages"; import { PricingEnforcedSpan } from "components/pricing_enforcers"; import type { ItemType, MenuItemType, SubMenuType } from "antd/es/menu/interface"; import type { MenuClickEventHandler } from "rc-menu/lib/interface"; -import constants from "oxalis/constants"; +import constants, { type AnnotationMutexStateEnum } from "oxalis/constants"; import { MaintenanceBanner, UpgradeVersionBanner } from "banners"; import { getAntdTheme, getSystemColorTheme } from "theme"; import { formatUserName } from "oxalis/model/accessors/user_accessor"; -import { isAnnotationOwner as isAnnotationOwnerAccessor } from "oxalis/model/accessors/annotation_accessor"; +import { + isAnnotationEditingAllowed, + isAnnotationOwner as isAnnotationOwnerAccessor, +} from "oxalis/model/accessors/annotation_accessor"; const { Header } = Layout; @@ -84,6 +87,7 @@ type StateProps = { hasOrganizations: boolean; othersMayEdit: boolean; allowUpdate: boolean; + annotationMutexState: AnnotationMutexStateEnum; isLockedByOwner: boolean; isAnnotationOwner: boolean; annotationOwnerName: string; @@ -811,6 +815,7 @@ function Navbar({ othersMayEdit, blockedByUser, allowUpdate, + annotationMutexState, annotationOwnerName, isLockedByOwner, navbarHeight, @@ -877,7 +882,10 @@ function Navbar({ menuItems.push(getTimeTrackingMenu(collapseAllNavItems)); } - if (othersMayEdit && !allowUpdate && !isLockedByOwner) { + if ( + othersMayEdit && + !isAnnotationEditingAllowed(allowUpdate, isLockedByOwner, annotationMutexState) + ) { trailingNavItems.push( ({ othersMayEdit: state.tracing.othersMayEdit, blockedByUser: state.tracing.blockedByUser, allowUpdate: state.tracing.restrictions.allowUpdate, + annotationMutexState: state.tracing.annotationMutexState, isLockedByOwner: state.tracing.isLockedByOwner, annotationOwnerName: formatUserName(state.activeUser, state.tracing.owner), isAnnotationOwner: isAnnotationOwnerAccessor(state), diff --git a/frontend/javascripts/oxalis/constants.ts b/frontend/javascripts/oxalis/constants.ts index 3276804c564..47c2e844e56 100644 --- a/frontend/javascripts/oxalis/constants.ts +++ b/frontend/javascripts/oxalis/constants.ts @@ -266,6 +266,13 @@ export const NODE_ID_REF_REGEX = /#([0-9]+)/g; export const POSITION_REF_REGEX = /#\(([0-9]+,[0-9]+,[0-9]+)\)/g; const VIEWPORT_WIDTH = 376; +export enum AnnotationMutexStateEnum { + NOT_NEEDED = "NOT_NEEDED", + ACQUIRED_FROM_BEGINNING = "ACQUIRED_FROM_BEGINNING", + PENDING = "PENDING", + ACQUIRED = "ACQUIRED", +} + // ARBITRARY_CAM_DISTANCE has to be calculated such that with cam // angle 45°, the plane of width Constants.VIEWPORT_WIDTH fits exactly in the // viewport. diff --git a/frontend/javascripts/oxalis/default_state.ts b/frontend/javascripts/oxalis/default_state.ts index cee355bb9ad..98768d25443 100644 --- a/frontend/javascripts/oxalis/default_state.ts +++ b/frontend/javascripts/oxalis/default_state.ts @@ -8,6 +8,7 @@ import Constants, { TDViewDisplayModeEnum, InterpolationModeEnum, UnitLong, + AnnotationMutexStateEnum, } from "oxalis/constants"; import type { APIAllowedMode, @@ -48,6 +49,7 @@ const initialAnnotationInfo = { }, annotationType: "View" as APIAnnotationType, meshes: [], + annotationMutexState: AnnotationMutexStateEnum.NOT_NEEDED, }; const defaultState: OxalisState = { diff --git a/frontend/javascripts/oxalis/model/accessors/annotation_accessor.ts b/frontend/javascripts/oxalis/model/accessors/annotation_accessor.ts index 1949bbadd23..ffb20c71a16 100644 --- a/frontend/javascripts/oxalis/model/accessors/annotation_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/annotation_accessor.ts @@ -3,6 +3,7 @@ import type { OxalisState, Tracing } from "oxalis/store"; import { getVolumeTracingById } from "./volumetracing_accessor"; import type { APIAnnotationInfo } from "types/api_flow_types"; import type { EmptyObject } from "types/globals"; +import { AnnotationMutexStateEnum } from "oxalis/constants"; export function mayEditAnnotationProperties(state: OxalisState) { const { owner, restrictions } = state.tracing; @@ -13,7 +14,30 @@ export function mayEditAnnotationProperties(state: OxalisState) { restrictions.allowSave && activeUser && owner?.id === activeUser.id && - !state.tracing.isLockedByOwner + !state.tracing.isLockedByOwner && + !(state.tracing.annotationMutexState === AnnotationMutexStateEnum.PENDING) + ); +} + +export function isAnnotationEditingAllowedByFullState(state: OxalisState) { + return isAnnotationEditingAllowed( + state.tracing.restrictions.allowUpdate, + state.tracing.isLockedByOwner, + state.tracing.annotationMutexState, + ); +} + +export function isAnnotationEditingAllowed( + allowUpdate: boolean, + isLockedByOwner: boolean, + annotationMutexState: AnnotationMutexStateEnum, +) { + return ( + allowUpdate && + !isLockedByOwner && + [AnnotationMutexStateEnum.PENDING, AnnotationMutexStateEnum.ACQUIRED].includes( + annotationMutexState, + ) ); } diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts index 1aa7ff5e470..155e6357c7d 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.ts +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.ts @@ -13,7 +13,7 @@ import type { UserBoundingBoxWithoutId, UserBoundingBoxWithoutIdMaybe, } from "oxalis/store"; -import type { Vector3 } from "oxalis/constants"; +import type { AnnotationMutexStateEnum, Vector3 } from "oxalis/constants"; import _ from "lodash"; import type { Dispatch } from "redux"; import Deferred from "libs/async/deferred"; @@ -25,6 +25,7 @@ type SetAnnotationVisibilityAction = ReturnType; type SetAnnotationDescriptionAction = ReturnType; type SetAnnotationAllowUpdateAction = ReturnType; +type SetAnnotationMutexStateAction = ReturnType; type SetBlockedByUserAction = ReturnType; type SetUserBoundingBoxesAction = ReturnType; type FinishedResizingUserBoundingBoxAction = ReturnType< @@ -58,6 +59,7 @@ export type AnnotationActionTypes = | EditAnnotationLayerAction | SetAnnotationDescriptionAction | SetAnnotationAllowUpdateAction + | SetAnnotationMutexStateAction | SetBlockedByUserAction | SetUserBoundingBoxesAction | ChangeUserBoundingBoxAction @@ -134,7 +136,11 @@ export const setAnnotationAllowUpdateAction = (allowUpdate: boolean) => type: "SET_ANNOTATION_ALLOW_UPDATE", allowUpdate, }) as const; - +export const setAnnotationMutexStateAction = (mutexState: AnnotationMutexStateEnum) => + ({ + type: "SET_ANNOTATION_MUTEX_STATE", + mutexState, + }) as const; export const setBlockedByUserAction = (blockedByUser: APIUserCompact | null | undefined) => ({ type: "SET_BLOCKED_BY_USER", diff --git a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts index 0bc85460aa5..c423b58d002 100644 --- a/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/annotation_reducer.ts @@ -119,6 +119,12 @@ function AnnotationReducer(state: OxalisState, action: Action): OxalisState { allowUpdate, }); } + case "SET_ANNOTATION_MUTEX_STATE": { + const { mutexState } = action; + return updateKey(state, "tracing", { + annotationMutexState: mutexState, + }); + } case "SET_BLOCKED_BY_USER": { const { blockedByUser } = action; diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts index 16b3c90e4f0..0b0897e3863 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts @@ -48,6 +48,7 @@ import { getNodeKey, } from "oxalis/view/right-border-tabs/tree_hierarchy_view_helpers"; import type { MetadataEntryProto } from "types/api_flow_types"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; function SkeletonTracingReducer(state: OxalisState, action: Action): OxalisState { switch (action.type) { @@ -590,11 +591,10 @@ function SkeletonTracingReducer(state: OxalisState, action: Action): OxalisState } /** - * ATTENTION: The following actions are only executed if allowUpdate is true! + * ATTENTION: The following actions are only executed if isAnnotationEditingAllowed is true! */ const { restrictions } = state.tracing; - const { allowUpdate } = restrictions; - if (!allowUpdate) return state; + if (!isAnnotationEditingAllowedByFullState(state)) return state; switch (action.type) { case "CREATE_NODE": { diff --git a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts index f2ff54b4d7d..49c5ddfe968 100644 --- a/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/ui_reducer.ts @@ -7,6 +7,7 @@ import { getPreviousTool, } from "oxalis/model/reducers/reducer_helpers"; import { hideBrushReducer } from "oxalis/model/reducers/volumetracing_reducer_helpers"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; function UiReducer(state: OxalisState, action: Action): OxalisState { switch (action.type) { @@ -54,7 +55,7 @@ function UiReducer(state: OxalisState, action: Action): OxalisState { } case "SET_TOOL": { - if (!state.tracing.restrictions.allowUpdate) { + if (!isAnnotationEditingAllowedByFullState(state)) { return state; } @@ -62,7 +63,7 @@ function UiReducer(state: OxalisState, action: Action): OxalisState { } case "CYCLE_TOOL": { - if (!state.tracing.restrictions.allowUpdate) { + if (!isAnnotationEditingAllowedByFullState(state)) { return state; } diff --git a/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer_helpers.ts b/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer_helpers.ts index 96e6dc11da0..78d8149ab78 100644 --- a/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer_helpers.ts +++ b/frontend/javascripts/oxalis/model/reducers/volumetracing_reducer_helpers.ts @@ -22,6 +22,7 @@ import { setDirectionReducer } from "oxalis/model/reducers/flycam_reducer"; import { updateKey } from "oxalis/model/helpers/deep_update"; import { mapGroupsToGenerator } from "../accessors/skeletontracing_accessor"; import { getMaximumSegmentIdForLayer } from "../accessors/dataset_accessor"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; export function updateVolumeTracing( state: OxalisState, @@ -114,9 +115,10 @@ export function addToLayerReducer( volumeTracing: VolumeTracing, position: Vector3, ) { - const { allowUpdate } = state.tracing.restrictions; - - if (!allowUpdate || isVolumeAnnotationDisallowedForZoom(state.uiInformation.activeTool, state)) { + if ( + !isAnnotationEditingAllowedByFullState(state) || + isVolumeAnnotationDisallowedForZoom(state.uiInformation.activeTool, state) + ) { return state; } diff --git a/frontend/javascripts/oxalis/model/sagas/annotation_saga.tsx b/frontend/javascripts/oxalis/model/sagas/annotation_saga.tsx index c93961d49cd..c58c1b51ef3 100644 --- a/frontend/javascripts/oxalis/model/sagas/annotation_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/annotation_saga.tsx @@ -3,7 +3,7 @@ import _ from "lodash"; import type { Action } from "oxalis/model/actions/actions"; import { type EditAnnotationLayerAction, - setAnnotationAllowUpdateAction, + setAnnotationMutexStateAction, setBlockedByUserAction, type SetOthersMayEditForAnnotationAction, } from "oxalis/model/actions/annotation_actions"; @@ -37,7 +37,7 @@ import { getActiveMagIndexForLayer } from "oxalis/model/accessors/flycam_accesso import { Model } from "oxalis/singletons"; import Store from "oxalis/store"; import Toast from "libs/toast"; -import constants, { MappingStatusEnum } from "oxalis/constants"; +import constants, { AnnotationMutexStateEnum, MappingStatusEnum } from "oxalis/constants"; import messages from "messages"; import type { APIUserCompact } from "types/api_flow_types"; import { Button } from "antd"; @@ -240,7 +240,6 @@ export function* acquireAnnotationMutexMaybe(): Saga { const MUTEX_NOT_ACQUIRED_KEY = "MutexCouldNotBeAcquired"; const MUTEX_ACQUIRED_KEY = "AnnotationMutexAcquired"; let isInitialRequest = true; - let doesHaveMutexFromBeginning = false; let doesHaveMutex = false; let shallTryAcquireMutex = othersMayEdit; @@ -271,7 +270,7 @@ export function* acquireAnnotationMutexMaybe(): Saga { function* tryAcquireMutexContinuously(): Saga { while (shallTryAcquireMutex) { if (isInitialRequest) { - yield* put(setAnnotationAllowUpdateAction(false)); + yield* put(setAnnotationMutexStateAction(AnnotationMutexStateEnum.PENDING)); } try { const { canEdit, blockedByUser } = yield* retry( @@ -280,32 +279,29 @@ export function* acquireAnnotationMutexMaybe(): Saga { acquireAnnotationMutex, annotationId, ); + if (canEdit !== doesHaveMutex || isInitialRequest) { + doesHaveMutex = canEdit; + onMutexStateChanged(canEdit, blockedByUser); + } if (isInitialRequest && canEdit) { - doesHaveMutexFromBeginning = true; // Only set allow update to true in case the first try to get the mutex succeeded. - yield* put(setAnnotationAllowUpdateAction(true)); - } - if (!canEdit || !doesHaveMutexFromBeginning) { - // If the mutex could not be acquired anymore or the user does not have it from the beginning, set allow update to false. - doesHaveMutexFromBeginning = false; - yield* put(setAnnotationAllowUpdateAction(false)); + yield* put( + setAnnotationMutexStateAction(AnnotationMutexStateEnum.ACQUIRED_FROM_BEGINNING), + ); + } else if (!isInitialRequest && canEdit) { + yield* put(setAnnotationMutexStateAction(AnnotationMutexStateEnum.ACQUIRED)); } if (canEdit) { yield* put(setBlockedByUserAction(activeUser)); } else { yield* put(setBlockedByUserAction(blockedByUser)); } - if (canEdit !== doesHaveMutex || isInitialRequest) { - doesHaveMutex = canEdit; - onMutexStateChanged(canEdit, blockedByUser); - } } catch (error) { const wasCanceled = yield* cancelled(); if (!wasCanceled) { console.error("Error while trying to acquire mutex.", error); yield* put(setBlockedByUserAction(undefined)); - yield* put(setAnnotationAllowUpdateAction(false)); - doesHaveMutexFromBeginning = false; + yield* put(setAnnotationMutexStateAction(AnnotationMutexStateEnum.PENDING)); if (doesHaveMutex || isInitialRequest) { onMutexStateChanged(false, null); doesHaveMutex = false; @@ -329,7 +325,7 @@ export function* acquireAnnotationMutexMaybe(): Saga { runningTryAcquireMutexContinuouslySaga = yield* fork(tryAcquireMutexContinuously); } else { // othersMayEdit was turned off. The user editing it should be able to edit the annotation. - yield* put(setAnnotationAllowUpdateAction(true)); + yield* put(setAnnotationMutexStateAction(AnnotationMutexStateEnum.NOT_NEEDED)); } } yield* takeEvery("SET_OTHERS_MAY_EDIT_FOR_ANNOTATION", reactToOthersMayEditChanges); diff --git a/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts b/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts index 3839d8f48c3..23e980ad627 100644 --- a/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts @@ -83,6 +83,7 @@ import { takeEveryUnlessBusy } from "./saga_helpers"; import type { Action } from "../actions/actions"; import { isBigInt, isNumberMap, SoftError } from "libs/utils"; import { getCurrentMag } from "../accessors/flycam_accessor"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; function runSagaAndCatchSoftError(saga: (...args: any[]) => Saga) { return function* (...args: any[]) { @@ -319,8 +320,8 @@ function* handleSkeletonProofreadingAction(action: Action): Saga { return; } - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); - if (!allowUpdate) return; + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); + if (!allowEditing) return; const { sourceNodeId, targetNodeId } = action; const skeletonTracing = yield* select((state) => enforceSkeletonTracing(state.tracing)); @@ -691,8 +692,8 @@ function* handleProofreadMergeOrMinCut(action: Action) { return; } - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); - if (!allowUpdate) return; + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); + if (!allowEditing) return; const preparation = yield* call(prepareSplitOrMerge, false); if (!preparation) { @@ -884,8 +885,8 @@ function* handleProofreadCutFromNeighbors(action: Action) { // This action does not depend on the active agglomerate. Instead, it // only depends on the rightclicked agglomerate. - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); - if (!allowUpdate) return; + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); + if (!allowEditing) return; const preparation = yield* call(prepareSplitOrMerge, false); if (!preparation) { diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.ts b/frontend/javascripts/oxalis/model/sagas/save_saga.ts index cf87972ffec..8064af755a1 100644 --- a/frontend/javascripts/oxalis/model/sagas/save_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/save_saga.ts @@ -55,6 +55,7 @@ import type { VolumeTracing, } from "oxalis/store"; import { call, delay, fork, put, race, take, takeEvery } from "typed-redux-saga"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; const ONE_YEAR_MS = 365 * 24 * 3600 * 1000; @@ -430,11 +431,9 @@ export function* setupSavingForTracingType( ]); } - // The allowUpdate setting could have changed in the meantime - const allowUpdate = yield* select( - (state) => state.tracing.restrictions.allowUpdate && state.tracing.restrictions.allowSave, - ); - if (!allowUpdate) continue; + // The allowEditing setting could have changed in the meantime + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); + if (!allowEditing) continue; const tracing = (yield* select((state) => selectTracing(state, saveQueueType, tracingId))) as | VolumeTracing | SkeletonTracing; @@ -472,7 +471,8 @@ const VERSION_POLL_INTERVAL_SINGLE_EDITOR = 30 * 1000; function* watchForSaveConflicts() { function* checkForNewVersion() { const allowSave = yield* select( - (state) => state.tracing.restrictions.allowSave && state.tracing.restrictions.allowUpdate, + (state) => + state.tracing.restrictions.allowSave && isAnnotationEditingAllowedByFullState(state), ); if (allowSave) { // The active user is currently the only one that is allowed to mutate the annotation. diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index 287f71350dd..39ce1de1314 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -86,6 +86,7 @@ import { } from "oxalis/model/actions/connectome_actions"; import type { ServerSkeletonTracing } from "types/api_flow_types"; import memoizeOne from "memoize-one"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; function* centerActiveNode(action: Action): Saga { if ("suppressCentering" in action && action.suppressCentering) { @@ -347,8 +348,8 @@ function handleAgglomerateLoadingError( export function* loadAgglomerateSkeletonWithId( action: LoadAgglomerateSkeletonAction, ): Saga<[string, number] | null> { - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); - if (!allowUpdate) return null; + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); + if (!allowEditing) return null; const { layerName, mappingName, agglomerateId } = action; if (agglomerateId === 0) { diff --git a/frontend/javascripts/oxalis/model/sagas/task_saga.tsx b/frontend/javascripts/oxalis/model/sagas/task_saga.tsx index 05c86faa522..8c869d7873f 100644 --- a/frontend/javascripts/oxalis/model/sagas/task_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/task_saga.tsx @@ -26,6 +26,7 @@ import Store, { type RecommendedConfiguration } from "oxalis/store"; import Toast from "libs/toast"; import messages from "messages"; import renderIndependently from "libs/render_independently"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; function* maybeShowNewTaskTypeModal(taskType: APITaskType): Saga { // Users can acquire new tasks directly in the tracing view. Occasionally, @@ -133,8 +134,8 @@ export default function* watchTasksAsync(): Saga { yield* take("WK_READY"); const task = yield* select((state) => state.task); const activeUser = yield* select((state) => state.activeUser); - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); - if (task == null || activeUser == null || !allowUpdate) return; + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); + if (task == null || activeUser == null || !allowEditing) return; yield* call(maybeActivateMergerMode, task.type); const { lastTaskTypeId } = activeUser; const isDifferentTaskType = lastTaskTypeId == null || lastTaskTypeId !== task.type.id; @@ -148,9 +149,9 @@ export default function* watchTasksAsync(): Saga { } export function* warnAboutMagRestriction(): Saga { function* warnMaybe(): Saga { - const { allowUpdate } = yield* select((state) => state.tracing.restrictions); + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); - if (!allowUpdate) { + if (!allowEditing) { // If updates are not allowed in general, we return here, since we don't // want to show any warnings when the user cannot edit the annotation in the first // place (e.g., when viewing the annotation of another user). 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 3d1690b9363..559663a123b 100644 --- a/frontend/javascripts/oxalis/model/sagas/volume/volume_interpolation_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/volume/volume_interpolation_saga.ts @@ -39,6 +39,7 @@ import type { OxalisState } from "oxalis/store"; import { call, put } from "typed-redux-saga"; import { createVolumeLayer, getBoundingBoxForViewport, labelWithVoxelBuffer2D } from "./helpers"; import { requestBucketModificationInVolumeTracing } from "../saga_helpers"; +import { isAnnotationEditingAllowedByFullState } from "oxalis/model/accessors/annotation_accessor"; /* * This saga is capable of doing segment interpolation between two slices. @@ -263,8 +264,8 @@ function signedDist(arr: ndarray.NdArray) { } export default function* maybeInterpolateSegmentationLayer(): Saga { - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); - if (!allowUpdate) return; + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); + if (!allowEditing) return; const activeTool = yield* select((state) => state.uiInformation.activeTool); if (!ToolsWithInterpolationCapabilities.includes(activeTool)) { diff --git a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx index 12e1919ac31..55aaaf1a6ae 100644 --- a/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx +++ b/frontend/javascripts/oxalis/model/sagas/volumetracing_saga.tsx @@ -113,6 +113,7 @@ import maybeInterpolateSegmentationLayer from "./volume/volume_interpolation_sag import messages from "messages"; import { pushSaveQueueTransaction } from "../actions/save_actions"; import type { ActionPattern } from "redux-saga/effects"; +import { isAnnotationEditingAllowedByFullState } from "../accessors/annotation_accessor"; const OVERWRITE_EMPTY_WARNING_KEY = "OVERWRITE-EMPTY-WARNING"; @@ -176,9 +177,9 @@ function* warnTooLargeSegmentId(): Saga { export function* editVolumeLayerAsync(): Saga { yield* take("INITIALIZE_VOLUMETRACING"); - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); - while (allowUpdate) { + while (allowEditing) { const startEditingAction = yield* take("START_EDITING"); const wroteVoxelsBox = { value: false }; const busyBlockingInfo = yield* select((state) => state.uiInformation.busyBlockingInfo); @@ -402,9 +403,9 @@ function* getBoundingBoxForFloodFill( const FLOODFILL_PROGRESS_KEY = "FLOODFILL_PROGRESS_KEY"; export function* floodFill(): Saga { yield* take("INITIALIZE_VOLUMETRACING"); - const allowUpdate = yield* select((state) => state.tracing.restrictions.allowUpdate); + const allowEditing = yield* select((state) => isAnnotationEditingAllowedByFullState(state)); - while (allowUpdate) { + while (allowEditing) { const floodFillAction = yield* take("FLOOD_FILL"); if (floodFillAction.type !== "FLOOD_FILL") { diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index 26fb97c0dd0..7aafd6fa894 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -47,6 +47,7 @@ import type { OrthoViewWithoutTD, InterpolationMode, TreeType, + AnnotationMutexStateEnum, } from "oxalis/constants"; import type { BLEND_MODES, ControlModeEnum } from "oxalis/constants"; import type { Matrix4x4 } from "libs/mjs"; @@ -204,6 +205,7 @@ export type Annotation = { readonly othersMayEdit: boolean; readonly blockedByUser: APIUserCompact | null | undefined; readonly isLockedByOwner: boolean; + readonly annotationMutexState: AnnotationMutexStateEnum; }; type TracingBase = { readonly createdTimestamp: number; diff --git a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx index a73f91a2e55..fc4fbed0217 100644 --- a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.tsx @@ -79,6 +79,7 @@ import { withAuthentication } from "admin/auth/authentication_modal"; import { PrivateLinksModal } from "./private_links_view"; import type { ItemType, SubMenuType } from "antd/es/menu/interface"; import CreateAnimationModal from "./create_animation_modal"; +import { isAnnotationEditingAllowedByFullState } from "oxalis/model/accessors/annotation_accessor"; const AsyncButtonWithAuthentication = withAuthentication( AsyncButton, @@ -91,6 +92,7 @@ type StateProps = { annotationType: APIAnnotationType; annotationId: string; restrictions: RestrictionsAndSettings; + allowEditing: boolean; task: Task | null | undefined; activeUser: APIUser | null | undefined; hasTracing: boolean; @@ -479,6 +481,7 @@ class TracingActionsView extends React.PureComponent { const { hasTracing, restrictions, + allowEditing, task, annotationType, annotationId, @@ -491,7 +494,7 @@ class TracingActionsView extends React.PureComponent { const isAnnotationOwner = activeUser && annotationOwner?.id === activeUser?.id; const copyAnnotationText = isAnnotationOwner ? "Duplicate" : "Copy To My Account"; const archiveButtonText = task ? "Finish and go to Dashboard" : "Archive"; - const saveButton = restrictions.allowUpdate + const saveButton = allowEditing ? [ hasTracing ? [ @@ -748,6 +751,7 @@ function mapStateToProps(state: OxalisState): StateProps { annotationType: state.tracing.annotationType, annotationId: state.tracing.annotationId, restrictions: state.tracing.restrictions, + allowEditing: isAnnotationEditingAllowedByFullState(state), annotationOwner: state.tracing.owner, task: state.task, activeUser: state.activeUser, diff --git a/frontend/javascripts/oxalis/view/action_bar_view.tsx b/frontend/javascripts/oxalis/view/action_bar_view.tsx index ef4a68f6a6b..de9517256f8 100644 --- a/frontend/javascripts/oxalis/view/action_bar_view.tsx +++ b/frontend/javascripts/oxalis/view/action_bar_view.tsx @@ -40,6 +40,7 @@ import ButtonComponent from "./components/button_component"; import { setAIJobModalStateAction } from "oxalis/model/actions/ui_actions"; import { type StartAIJobModalState, StartAIJobModal } from "./action-bar/starting_job_modals"; import { isUserAdminOrTeamManager } from "libs/utils"; +import { isAnnotationEditingAllowedByFullState } from "oxalis/model/accessors/annotation_accessor"; const VersionRestoreWarning = ( ({ controlMode: state.temporaryConfiguration.controlMode, showVersionRestore: state.uiInformation.showVersionRestore, hasSkeleton: state.tracing.skeleton != null, - isReadOnly: !state.tracing.restrictions.allowUpdate, + isReadOnly: !isAnnotationEditingAllowedByFullState(state), is2d: is2dDataset(state.dataset), viewMode: state.temporaryConfiguration.viewMode, aiJobModalState: state.uiInformation.aIJobModalState, diff --git a/frontend/javascripts/oxalis/view/context_menu.tsx b/frontend/javascripts/oxalis/view/context_menu.tsx index 634de6a7c59..6aae0ff23c4 100644 --- a/frontend/javascripts/oxalis/view/context_menu.tsx +++ b/frontend/javascripts/oxalis/view/context_menu.tsx @@ -132,6 +132,7 @@ import { hideContextMenuAction, setActiveUserBoundingBoxId } from "oxalis/model/ 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"; +import { isAnnotationEditingAllowedByFullState } from "oxalis/model/accessors/annotation_accessor"; type ContextMenuContextValue = React.MutableRefObject | null; export const ContextMenuContext = createContext(null); @@ -162,7 +163,7 @@ type StateProps = { useLegacyBindings: boolean; userBoundingBoxes: Array; mappingInfo: ActiveMappingInfo; - allowUpdate: boolean; + allowEditing: boolean; segments: SegmentMap | null | undefined; }; type Props = OwnProps & StateProps; @@ -557,7 +558,7 @@ function getNodeContextMenuOptions({ useLegacyBindings, volumeTracing, infoRows, - allowUpdate, + allowEditing, currentMeshFile, }: NodeContextMenuOptionsProps): ItemType[] { const state = Store.getState(); @@ -611,7 +612,7 @@ function getNodeContextMenuOptions({ label: "Select this Node", }, getMaybeMinCutItem(clickedTree, volumeTracing, userBoundingBoxes, isVolumeModificationAllowed), - ...(allowUpdate + ...(allowEditing ? [ { key: "merge-trees", @@ -742,7 +743,7 @@ function getNodeContextMenuOptions({ measureAndShowFullTreeLength(clickedTree.treeId, clickedTree.name, voxelSize.unit), label: "Path Length of this Tree", }, - allowUpdate + allowEditing ? { key: "hide-tree", onClick: () => Store.dispatch(setTreeVisibilityAction(clickedTree.treeId, false)), @@ -761,7 +762,7 @@ function getBoundingBoxMenuOptions({ clickedBoundingBoxId, userBoundingBoxes, hideContextMenu, - allowUpdate, + allowEditing, }: NoNodeContextMenuProps): ItemType[] { if (globalPosition == null) return []; @@ -779,7 +780,7 @@ function getBoundingBoxMenuOptions({ ), }; - if (!allowUpdate && clickedBoundingBoxId != null) { + if (!allowEditing && clickedBoundingBoxId != null) { const hideBoundingBoxMenuItem: MenuItemType = { key: "hide-bounding-box", onClick: () => { @@ -790,7 +791,7 @@ function getBoundingBoxMenuOptions({ return [hideBoundingBoxMenuItem]; } - if (!allowUpdate) { + if (!allowEditing) { return []; } @@ -926,7 +927,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] currentConnectomeFile, mappingInfo, infoRows, - allowUpdate, + allowEditing, } = props; const state = Store.getState(); @@ -1016,7 +1017,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] const isVolumeBasedToolActive = VolumeTools.includes(activeTool); const isBoundingBoxToolActive = activeTool === AnnotationToolEnum.BOUNDING_BOX; const skeletonActions: ItemType[] = - skeletonTracing != null && globalPosition != null && allowUpdate + skeletonTracing != null && globalPosition != null && allowEditing ? [ { key: "create-node", @@ -1216,7 +1217,7 @@ function getNoNodeContextMenuOptions(props: NoNodeContextMenuProps): ItemType[] focusInSegmentListItem, loadPrecomputedMeshItem, computeMeshAdHocItem, - allowUpdate && !disabledVolumeInfo.FILL_CELL.isDisabled + allowEditing && !disabledVolumeInfo.FILL_CELL.isDisabled ? { key: "fill-cell", onClick: () => handleFloodFillFromGlobalPosition(globalPosition, viewport), @@ -1353,7 +1354,7 @@ function WkContextMenu() { voxelSize: state.dataset.dataSource.scale, activeTool: state.uiInformation.activeTool, dataset: state.dataset, - allowUpdate: state.tracing.restrictions.allowUpdate, + allowEditing: isAnnotationEditingAllowedByFullState(state), visibleSegmentationLayer, currentMeshFile: visibleSegmentationLayer != null diff --git a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx index 59b34a3300a..d0b6d1f25f6 100644 --- a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx +++ b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx @@ -44,6 +44,7 @@ import { import { layoutEmitter, getLayoutConfig } from "./layout_persistence"; import BorderToggleButton from "../components/border_toggle_button"; import FastTooltip from "components/fast_tooltip"; +import { isAnnotationEditingAllowedByFullState } from "oxalis/model/accessors/annotation_accessor"; const { Footer } = Layout; @@ -51,7 +52,7 @@ type Model = InstanceType; type Action = InstanceType; type StateProps = { displayScalebars: boolean; - isUpdateTracingAllowed: boolean; + isEditingTracingAllowed: boolean; busyBlockingInfo: BusyBlockingInfo; }; type OwnProps = { @@ -315,7 +316,7 @@ class FlexLayoutWrapper extends React.PureComponent { } renderViewport(id: string): React.ReactNode | null | undefined { - const { displayScalebars, isUpdateTracingAllowed, busyBlockingInfo } = this.props; + const { displayScalebars, isEditingTracingAllowed, busyBlockingInfo } = this.props; switch (id) { case OrthoViews.PLANE_XY: @@ -348,7 +349,7 @@ class FlexLayoutWrapper extends React.PureComponent { busyBlockingInfo={busyBlockingInfo} viewportID={ArbitraryViews.arbitraryViewport} > - {isUpdateTracingAllowed ? : null} + {isEditingTracingAllowed ? : null} ); } @@ -577,7 +578,7 @@ class FlexLayoutWrapper extends React.PureComponent { function mapStateToProps(state: OxalisState): StateProps { return { displayScalebars: state.userConfiguration.displayScalebars, - isUpdateTracingAllowed: state.tracing.restrictions.allowUpdate, + isEditingTracingAllowed: isAnnotationEditingAllowedByFullState(state), busyBlockingInfo: state.uiInformation.busyBlockingInfo, }; } diff --git a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx index f196944b1d5..6eb360f3d7b 100644 --- a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx +++ b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx @@ -46,6 +46,7 @@ import { determineLayout } from "./default_layout_configs"; import FlexLayoutWrapper from "./flex_layout_wrapper"; import { FloatingMobileControls } from "./floating_mobile_controls"; import app from "app"; +import { isAnnotationEditingAllowedByFullState } from "oxalis/model/accessors/annotation_accessor"; const { Sider } = Layout; @@ -256,8 +257,11 @@ class TracingLayoutView extends React.PureComponent { this.props.is2d, ); const currentLayoutNames = this.getLayoutNamesFromCurrentView(layoutType); - const { isDatasetOnScratchVolume, isUpdateTracingAllowed, distanceMeasurementTooltipPosition } = - this.props; + const { + isDatasetOnScratchVolume, + isEditingTracingAllowed, + distanceMeasurementTooltipPosition, + } = this.props; const createNewTracing = async ( files: Array, @@ -285,8 +289,8 @@ class TracingLayoutView extends React.PureComponent { )} {
{this.props.showVersionRestore ? ( - + ) : null} @@ -398,7 +402,7 @@ function mapStateToProps(state: OxalisState) { return { viewMode: state.temporaryConfiguration.viewMode, autoSaveLayouts: state.userConfiguration.autoSaveLayouts, - isUpdateTracingAllowed: state.tracing.restrictions.allowUpdate, + isEditingTracingAllowed: isAnnotationEditingAllowedByFullState(state), showVersionRestore: state.uiInformation.showVersionRestore, storedLayouts: state.uiInformation.storedLayouts, isDatasetOnScratchVolume: state.dataset.dataStore.isScratch, diff --git a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx index 29fb787852b..49d00f98fd7 100644 --- a/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx +++ b/frontend/javascripts/oxalis/view/left-border-tabs/layer_settings_tab.tsx @@ -131,6 +131,7 @@ import { getDefaultLayerViewConfiguration, } from "types/schemas/dataset_view_configuration.schema"; import defaultState from "oxalis/default_state"; +import { isAnnotationEditingAllowedByFullState } from "oxalis/model/accessors/annotation_accessor"; type DatasetSettingsProps = { userConfiguration: UserConfiguration; @@ -151,6 +152,7 @@ type DatasetSettingsProps = { onChangeUser: (key: keyof UserConfiguration, value: any) => void; reloadHistogram: (layerName: string) => void; tracing: Tracing; + allowEditing: boolean; task: Task | null | undefined; onEditAnnotationLayer: (tracingId: string, layerProperties: EditableLayerProperties) => void; controlMode: ControlMode; @@ -1505,8 +1507,7 @@ class DatasetSettings extends React.PureComponent { {segmentationLayerSettings} {this.getSkeletonLayer()} - {this.props.tracing.restrictions.allowUpdate && - this.props.controlMode === ControlModeEnum.TRACE ? ( + {this.props.allowEditing && this.props.controlMode === ControlModeEnum.TRACE ? ( <> @@ -1518,7 +1519,7 @@ class DatasetSettings extends React.PureComponent { ) : null} - {this.props.tracing.restrictions.allowUpdate && canBeMadeHybrid ? ( + {this.props.allowEditing && canBeMadeHybrid ? ( } @@ -282,7 +282,7 @@ class NmlUploadZoneContainer extends React.PureComponent { diff --git a/frontend/javascripts/oxalis/view/version_entry.tsx b/frontend/javascripts/oxalis/view/version_entry.tsx index c792f7171d6..76d7a5b7d1c 100644 --- a/frontend/javascripts/oxalis/view/version_entry.tsx +++ b/frontend/javascripts/oxalis/view/version_entry.tsx @@ -275,7 +275,7 @@ function getDescriptionForBatch(actions: Array): Description type Props = { actions: Array; - allowUpdate: boolean; + allowEditing: boolean; version: number; isNewest: boolean; isActive: boolean; @@ -285,7 +285,7 @@ type Props = { }; export default function VersionEntry({ actions, - allowUpdate, + allowEditing, version, isNewest, isActive, @@ -309,7 +309,7 @@ export default function VersionEntry({ type="primary" onClick={() => onRestoreVersion(version)} > - {allowUpdate ? "Restore" : "Download"} + {allowEditing ? "Restore" : "Download"} ); const { description, icon } = getDescriptionForBatch(actions); diff --git a/frontend/javascripts/oxalis/view/version_entry_group.tsx b/frontend/javascripts/oxalis/view/version_entry_group.tsx index 4d5783fc148..ec4e869503c 100644 --- a/frontend/javascripts/oxalis/view/version_entry_group.tsx +++ b/frontend/javascripts/oxalis/view/version_entry_group.tsx @@ -8,7 +8,7 @@ import { CaretDownOutlined, CaretRightOutlined } from "@ant-design/icons"; type Props = { batches: APIUpdateActionBatch[]; - allowUpdate: boolean; + allowEditing: boolean; newestVersion: number; activeVersion: number; onRestoreVersion: (arg0: number) => Promise; @@ -65,7 +65,7 @@ export default class VersionEntryGroup extends React.Component { render() { const { batches, - allowUpdate, + allowEditing, newestVersion, activeVersion, onRestoreVersion, @@ -85,7 +85,7 @@ export default class VersionEntryGroup extends React.Component { {this.state.expanded || !containsMultipleBatches ? batches.map((batch) => ( _.max(versions.map((batch) => batch.version)) || 0; - if (props.allowUpdate) { + if (props.allowEditing) { Store.dispatch( setVersionNumberAction( getNewestVersion(), @@ -336,7 +337,7 @@ function InnerVersionList(props: Props & { newestVersion: number }) { ) : ( diff --git a/frontend/javascripts/oxalis/view/version_view.tsx b/frontend/javascripts/oxalis/view/version_view.tsx index 8068ad59be6..96915840e54 100644 --- a/frontend/javascripts/oxalis/view/version_view.tsx +++ b/frontend/javascripts/oxalis/view/version_view.tsx @@ -18,12 +18,12 @@ type StateProps = { tracing: Tracing; }; type OwnProps = { - allowUpdate: boolean; + allowEditing: boolean; }; type Props = StateProps & OwnProps; type State = { activeTracingType: TracingType; - initialAllowUpdate: boolean; + initialAllowEditing: boolean; }; class VersionView extends React.Component { @@ -31,18 +31,19 @@ class VersionView extends React.Component { activeTracingType: this.props.tracing.skeleton != null ? TracingTypeEnum.skeleton : TracingTypeEnum.volume, // Remember whether the tracing could originally be updated - initialAllowUpdate: this.props.allowUpdate, + initialAllowEditing: this.props.allowEditing, }; componentWillUnmount() { - Store.dispatch(setAnnotationAllowUpdateAction(this.state.initialAllowUpdate)); + // TODOM: Fix this. It does not set the allow update correctly as allow editing is a combination of setting. + Store.dispatch(setAnnotationAllowUpdateAction(this.state.initialAllowEditing)); } handleClose = async () => { // This will load the newest version of both skeleton and volume tracings await previewVersion(); Store.dispatch(setVersionRestoreVisibilityAction(false)); - Store.dispatch(setAnnotationAllowUpdateAction(this.state.initialAllowUpdate)); + Store.dispatch(setAnnotationAllowUpdateAction(this.state.initialAllowEditing)); }; onChangeTab = (activeKey: string) => { @@ -62,7 +63,7 @@ class VersionView extends React.Component { ), }); @@ -75,7 +76,7 @@ class VersionView extends React.Component { ), })), @@ -92,7 +93,7 @@ class VersionView extends React.Component { ), })),