From 0bd7c7bacee42932cb9881c04ac6a203308c26ae Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 23 Jan 2019 16:17:00 +0100 Subject: [PATCH] Allow to download NMLs of older versions for read-only tracings (#3660) * allow version restore view in read-only tracings, offer to download NML of older versions * use correct version parameters for nml download * annotation download: pass on version parameters to tracingstore * scalafmt * rename tracingType and APITracingType to annotationType where appropriate * update changelog * apply PR feedback --- CHANGELOG.md | 1 + .../javascripts/admin/admin_rest_api.js | 47 ++++++++++------ .../javascripts/admin/api_flow_types.js | 14 ++--- .../dashboard/explorative_annotations_view.js | 28 +++++----- .../javascripts/oxalis/api/api_latest.js | 10 ++-- app/assets/javascripts/oxalis/api/api_v2.js | 10 ++-- app/assets/javascripts/oxalis/controller.js | 6 +-- .../oxalis/controller/url_manager.js | 6 +-- app/assets/javascripts/oxalis/model.js | 6 +-- .../oxalis/model/helpers/nml_helpers.js | 4 +- .../oxalis/model/reducers/reducer_helpers.js | 4 +- .../skeletontracing_reducer_helpers.js | 2 +- .../oxalis/model/sagas/annotation_saga.js | 2 +- .../oxalis/model_initialization.js | 6 +-- app/assets/javascripts/oxalis/store.js | 8 +-- .../view/action-bar/merge_modal_view.js | 8 +-- .../view/action-bar/tracing_actions_view.js | 53 +++++++++---------- .../oxalis/view/action_bar_view.js | 16 ++---- .../view/layouting/tracing_layout_view.js | 10 ++-- .../view/right-menu/dataset_info_tab_view.js | 9 ++-- .../javascripts/oxalis/view/version_entry.js | 4 +- .../oxalis/view/version_entry_group.js | 3 ++ .../javascripts/oxalis/view/version_list.js | 23 +++++--- .../javascripts/oxalis/view/version_view.js | 29 ++++++++-- app/assets/javascripts/router.js | 14 ++--- .../backend-snapshot-tests/annotations.e2e.js | 28 +++++----- .../test/controller/url_manager.spec.js | 12 ++--- .../javascripts/test/helpers/apiHelpers.js | 6 +-- app/assets/javascripts/test/libs/nml.spec.js | 2 +- .../javascripts/test/model/model.spec.js | 8 +-- .../reducers/skeletontracing_reducer.spec.js | 2 +- .../reducers/volumetracing_reducer.spec.js | 2 +- .../javascripts/test/sagas/save_saga.spec.js | 2 +- .../test/sagas/skeletontracing_saga.spec.js | 2 +- .../test/sagas/volumetracing_saga.spec.js | 2 +- app/controllers/AnnotationIOController.scala | 42 ++++++++------- .../annotation/TracingStoreRpcClient.scala | 7 ++- conf/webknossos.latest.routes | 2 +- 38 files changed, 245 insertions(+), 195 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25225e5bf90..ae0b7e66651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). ### Added - Added the possibility to disable that the current layout is saved automatically when changing it. Instead, the layout can be saved explicitly. [#3620](https://github.com/scalableminds/webknossos/pull/3620) +- Added the possibility to open the version restore view for read-only tracings. Older versions can be previewed and be downloaded as NML. [#3660](https://github.com/scalableminds/webknossos/pull/3660) ### Changed diff --git a/app/assets/javascripts/admin/admin_rest_api.js b/app/assets/javascripts/admin/admin_rest_api.js index 20ad4bea275..237a5ae5d1c 100644 --- a/app/assets/javascripts/admin/admin_rest_api.js +++ b/app/assets/javascripts/admin/admin_rest_api.js @@ -4,7 +4,7 @@ import _ from "lodash"; import { type APIActiveUser, type APIAnnotation, - type APIAnnotationTypeCompact, + type APIAnnotationCompact, type APIAnnotationWithTask, type APIBuildInfo, type APIDataSource, @@ -30,8 +30,8 @@ import { type APITimeInterval, type APITimeTracking, type APITracingStore, - type APITracingType, - APITracingTypeEnum, + type APIAnnotationType, + APIAnnotationTypeEnum, type APIUpdateActionBatch, type APIUser, type APIUserLoggedTime, @@ -422,7 +422,7 @@ export async function updateTask(taskId: string, task: NewTask): Promise { - return finishAnnotation(annotationId, APITracingTypeEnum.Task); + return finishAnnotation(annotationId, APIAnnotationTypeEnum.Task); } export function transferTask(annotationId: string, userId: string): Promise { @@ -454,7 +454,7 @@ export async function getUsersWithActiveTasks(projectName: string): Promise> { +): Promise> { return Request.receiveJSON( `/api/user/annotations?isFinished=${isFinished.toString()}&pageNumber=${pageNumber}`, ); @@ -464,7 +464,7 @@ export function getCompactAnnotationsForUser( userId: string, isFinished: boolean, pageNumber: number = 0, -): Promise> { +): Promise> { return Request.receiveJSON( `/api/users/${userId}/annotations?isFinished=${isFinished.toString()}&pageNumber=${pageNumber}`, ); @@ -472,7 +472,7 @@ export function getCompactAnnotationsForUser( export function reOpenAnnotation( annotationId: string, - annotationType: APITracingType, + annotationType: APIAnnotationType, ): Promise { return Request.receiveJSON(`/api/annotations/${annotationType}/${annotationId}/reopen`, { method: "PATCH", @@ -488,7 +488,7 @@ export type EditableAnnotation = { export function editAnnotation( annotationId: string, - annotationType: APITracingType, + annotationType: APIAnnotationType, data: $Shape, ): Promise { return Request.sendJSONReceiveJSON(`/api/annotations/${annotationType}/${annotationId}/edit`, { @@ -499,7 +499,7 @@ export function editAnnotation( export function finishAnnotation( annotationId: string, - annotationType: APITracingType, + annotationType: APIAnnotationType, ): Promise { return Request.receiveJSON(`/api/annotations/${annotationType}/${annotationId}/finish`, { method: "PATCH", @@ -508,7 +508,7 @@ export function finishAnnotation( export function resetAnnotation( annotationId: string, - annotationType: APITracingType, + annotationType: APIAnnotationType, ): Promise { return Request.receiveJSON(`/api/annotations/${annotationType}/${annotationId}/reset`, { method: "PUT", @@ -517,7 +517,7 @@ export function resetAnnotation( export function deleteAnnotation( annotationId: string, - annotationType: APITracingType, + annotationType: APIAnnotationType, ): Promise<{ messages: Array }> { return Request.receiveJSON(`/api/annotations/${annotationType}/${annotationId}`, { method: "DELETE", @@ -537,17 +537,17 @@ export function finishAllAnnotations( export function copyAnnotationToUserAccount( annotationId: string, - tracingType: APITracingType, + annotationType: APIAnnotationType, ): Promise { - const url = `/api/annotations/${tracingType}/${annotationId}/duplicate`; + const url = `/api/annotations/${annotationType}/${annotationId}/duplicate`; return Request.receiveJSON(url, { method: "POST" }); } export function getAnnotationInformation( annotationId: string, - tracingType: APITracingType, + annotationType: APIAnnotationType, ): Promise { - const infoUrl = `/api/annotations/${tracingType}/${annotationId}/info`; + const infoUrl = `/api/annotations/${annotationType}/${annotationId}/info`; return Request.receiveJSON(infoUrl); } @@ -625,6 +625,23 @@ export function convertToHybridTracing(annotationId: string): Promise { }); } +export async function downloadNml( + annotationId: string, + annotationType: APIAnnotationType, + versions?: Versions = {}, +) { + const possibleVersionString = Object.entries(versions) + // $FlowFixMe Flow returns val as mixed here due to the use of Object.entries + .map(([key, val]) => `${key}Version=${val}`) + .join("&"); + const win = window.open("about:blank", "_blank"); + win.document.body.innerHTML = messages["download.wait"]; + + const downloadUrl = `/api/annotations/${annotationType}/${annotationId}/download?${possibleVersionString}`; + win.location.href = downloadUrl; + win.document.body.innerHTML = messages["download.close_window"]; +} + // ### Datasets export async function getDatasets(): Promise> { const datasets = await Request.receiveJSON("/api/datasets"); diff --git a/app/assets/javascripts/admin/api_flow_types.js b/app/assets/javascripts/admin/api_flow_types.js index 8e466d9120c..43ec071b3b8 100644 --- a/app/assets/javascripts/admin/api_flow_types.js +++ b/app/assets/javascripts/admin/api_flow_types.js @@ -174,7 +174,7 @@ export type APISettings = {| +somaClickingAllowed: boolean, |}; -export const APITracingTypeEnum = Enum.make({ +export const APIAnnotationTypeEnum = Enum.make({ Explorational: "Explorational", Task: "Task", View: "View", @@ -183,7 +183,7 @@ export const APITracingTypeEnum = Enum.make({ CompoundTaskType: "CompoundTaskType", }); -export type APITracingType = $Keys; +export type APIAnnotationType = $Keys; export type APITaskType = { +id: string, @@ -266,7 +266,7 @@ export type APITask = { +directLinks?: Array, }; -export type APIAnnotationTypeCompact = { +export type APIAnnotationCompact = { +tracing: { +skeleton: ?string, +volume: ?string, @@ -283,7 +283,7 @@ export type APIAnnotationTypeCompact = { +stats: SkeletonTracingStats | {||}, +tags: Array, +tracingTime: ?number, - +typ: APITracingType, + +typ: APIAnnotationType, }; export type LocalMeshMetaData = {| @@ -304,7 +304,7 @@ export type MeshMetaData = {| ...RemoteMeshMetaData, |}; -type APIAnnotationTypeBase = APIAnnotationTypeCompact & { +type APIAnnotationBase = APIAnnotationCompact & { +dataStore: APIDataStore, +tracingStore: APITracingStore, +restrictions: APIRestrictions, @@ -313,11 +313,11 @@ type APIAnnotationTypeBase = APIAnnotationTypeCompact & { +meshes: Array, }; -export type APIAnnotation = APIAnnotationTypeBase & { +export type APIAnnotation = APIAnnotationBase & { +task: ?APITask, }; -export type APIAnnotationWithTask = APIAnnotationTypeBase & { +export type APIAnnotationWithTask = APIAnnotationBase & { +task: APITask, }; diff --git a/app/assets/javascripts/dashboard/explorative_annotations_view.js b/app/assets/javascripts/dashboard/explorative_annotations_view.js index 0a6aa46128f..2066f615e49 100644 --- a/app/assets/javascripts/dashboard/explorative_annotations_view.js +++ b/app/assets/javascripts/dashboard/explorative_annotations_view.js @@ -9,7 +9,7 @@ import * as React from "react"; import _ from "lodash"; import update from "immutability-helper"; -import type { APIAnnotationTypeCompact } from "admin/api_flow_types"; +import type { APIAnnotationCompact } from "admin/api_flow_types"; import { AnnotationContentTypes } from "oxalis/constants"; import { AsyncLink } from "components/async_clickables"; import { @@ -36,11 +36,11 @@ import { trackAction } from "oxalis/model/helpers/analytics"; const { Column } = Table; const { Search } = Input; -const typeHint: APIAnnotationTypeCompact[] = []; +const typeHint: APIAnnotationCompact[] = []; const pageLength: number = 1000; export type TracingModeState = { - tracings: Array, + tracings: Array, lastLoadedPage: number, loadedAllTracings: boolean, }; @@ -178,7 +178,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { ); }; - finishOrReopenTracing = async (type: "finish" | "reopen", tracing: APIAnnotationTypeCompact) => { + finishOrReopenTracing = async (type: "finish" | "reopen", tracing: APIAnnotationCompact) => { const newTracing = type === "finish" ? await finishAnnotation(tracing.id, tracing.typ) @@ -205,7 +205,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { } }; - renderActions = (tracing: APIAnnotationTypeCompact) => { + renderActions = (tracing: APIAnnotationCompact) => { if (tracing.typ !== "Explorational") { return null; } @@ -240,7 +240,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { } }; - getCurrentTracings(): Array { + getCurrentTracings(): Array { return this.getCurrentModeState().tracings; } @@ -248,7 +248,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { this.setState({ searchQuery: event.target.value }); }; - renameTracing(tracing: APIAnnotationTypeCompact, name: string) { + renameTracing(tracing: APIAnnotationCompact, name: string) { const tracings = this.getCurrentTracings(); const newTracings = tracings.map(currentTracing => { @@ -310,7 +310,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { }; editTagFromAnnotation = ( - annotation: APIAnnotationTypeCompact, + annotation: APIAnnotationCompact, shouldAddTag: boolean, tag: string, event: SyntheticInputEvent<>, @@ -359,7 +359,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { ); } - renderNameWithDescription(tracing: APIAnnotationTypeCompact) { + renderNameWithDescription(tracing: APIAnnotationCompact) { const hasDescription = tracing.description !== ""; const markdownDescription = (
@@ -403,7 +403,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { formatHash(tracing.id)} + render={(__, tracing: APIAnnotationCompact) => formatHash(tracing.id)} sorter={Utils.localeCompareBy(typeHint, annotation => annotation.id)} className="monospace-id" /> @@ -411,13 +411,13 @@ class ExplorativeAnnotationsView extends React.PureComponent { title="Name" dataIndex="name" sorter={Utils.localeCompareBy(typeHint, annotation => annotation.name)} - render={(name: string, tracing: APIAnnotationTypeCompact) => + render={(name: string, tracing: APIAnnotationCompact) => this.renderNameWithDescription(tracing) } /> + render={(__, annotation: APIAnnotationCompact) => // Flow doesn't recognize that stats must contain the nodeCount if the treeCount is != null annotation.stats.treeCount != null && annotation.stats.nodeCount != null && @@ -446,7 +446,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { title="Tags" dataIndex="tags" width={500} - render={(tags: Array, annotation: APIAnnotationTypeCompact) => ( + render={(tags: Array, annotation: APIAnnotationCompact) => (
{tags.map(tag => ( { title="Actions" className="nowrap" key="action" - render={(__, tracing: APIAnnotationTypeCompact) => this.renderActions(tracing)} + render={(__, tracing: APIAnnotationCompact) => this.renderActions(tracing)} /> ); diff --git a/app/assets/javascripts/oxalis/api/api_latest.js b/app/assets/javascripts/oxalis/api/api_latest.js index eb13cc67648..4b4af7d216e 100644 --- a/app/assets/javascripts/oxalis/api/api_latest.js +++ b/app/assets/javascripts/oxalis/api/api_latest.js @@ -48,7 +48,7 @@ import type { Tracing, SkeletonTracing, VolumeTracing, - TracingTypeTracing, + AnnotationType, Mapping, TreeGroupTypeFlat, } from "oxalis/store"; @@ -368,11 +368,11 @@ class TracingApi { this.isFinishing = true; const state = Store.getState(); - const { tracingType, annotationId } = state.tracing; + const { annotationType, annotationId } = state.tracing; const { task } = state; await Model.save(); - await finishAnnotation(annotationId, tracingType); + await finishAnnotation(annotationId, annotationType); try { const annotation = await requestTask(); @@ -409,7 +409,7 @@ class TracingApi { * */ async restart( - newTracingType: TracingTypeTracing, + newAnnotationType: AnnotationType, newAnnotationId: string, newControlMode: ControlMode, versions?: Versions, @@ -420,7 +420,7 @@ class TracingApi { Store.dispatch(restartSagaAction()); UrlManager.reset(); await Model.fetch( - newTracingType, + newAnnotationType, { annotationId: newAnnotationId, type: newControlMode }, false, versions, diff --git a/app/assets/javascripts/oxalis/api/api_v2.js b/app/assets/javascripts/oxalis/api/api_v2.js index ceb8ada40c8..b9875bd6b2d 100644 --- a/app/assets/javascripts/oxalis/api/api_v2.js +++ b/app/assets/javascripts/oxalis/api/api_v2.js @@ -39,7 +39,7 @@ import type { Tracing, SkeletonTracing, VolumeTracing, - TracingTypeTracing, + AnnotationType, Mapping, } from "oxalis/store"; import { overwriteAction } from "oxalis/model/helpers/overwrite_action_middleware"; @@ -265,11 +265,11 @@ class TracingApi { */ async finishAndGetNextTask() { const state = Store.getState(); - const { tracingType, annotationId } = state.tracing; + const { annotationType, annotationId } = state.tracing; const { task } = state; await Model.save(); - await finishAnnotation(annotationId, tracingType); + await finishAnnotation(annotationId, annotationType); try { const annotation = await requestTask(); @@ -304,7 +304,7 @@ class TracingApi { * */ async restart( - newTracingType: TracingTypeTracing, + newAnnotationType: AnnotationType, newAnnotationId: string, newControlMode: ControlMode, ) { @@ -314,7 +314,7 @@ class TracingApi { Store.dispatch(restartSagaAction()); UrlManager.reset(); await Model.fetch( - newTracingType, + newAnnotationType, { annotationId: newAnnotationId, type: newControlMode }, false, ); diff --git a/app/assets/javascripts/oxalis/controller.js b/app/assets/javascripts/oxalis/controller.js index e759c7f02ef..9b2baecf5d6 100644 --- a/app/assets/javascripts/oxalis/controller.js +++ b/app/assets/javascripts/oxalis/controller.js @@ -25,7 +25,7 @@ import PlaneController from "oxalis/controller/viewmodes/plane_controller"; import Store, { type OxalisState, type TraceOrViewCommand, - type TracingTypeTracing, + type AnnotationType, } from "oxalis/store"; import Toast from "libs/toast"; import UrlManager from "oxalis/controller/url_manager"; @@ -42,7 +42,7 @@ type StateProps = { type Props = { history: RouterHistory, - initialTracingType: TracingTypeTracing, + initialAnnotationType: AnnotationType, initialCommandType: TraceOrViewCommand, } & StateProps; @@ -84,7 +84,7 @@ class Controller extends React.PureComponent { // Preview a working tracing version if the showVersionRestore URL parameter is supplied const versions = Utils.hasUrlParam("showVersionRestore") ? { skeleton: 1 } : undefined; - Model.fetch(this.props.initialTracingType, this.props.initialCommandType, true, versions) + Model.fetch(this.props.initialAnnotationType, this.props.initialCommandType, true, versions) .then(() => this.modelFetchDone()) .catch(error => { // Don't throw errors for errors already handled by the model. diff --git a/app/assets/javascripts/oxalis/controller/url_manager.js b/app/assets/javascripts/oxalis/controller/url_manager.js index fb63daf6698..cc77307a42d 100644 --- a/app/assets/javascripts/oxalis/controller/url_manager.js +++ b/app/assets/javascripts/oxalis/controller/url_manager.js @@ -120,14 +120,14 @@ class UrlManager { const { tracing } = Store.getState(); const hash = this.buildHash(tracing); - const newBaseUrl = updateTypeAndId(this.baseUrl, tracing.tracingType, tracing.annotationId); + const newBaseUrl = updateTypeAndId(this.baseUrl, tracing.annotationType, tracing.annotationId); return `${newBaseUrl}#${hash}`; } } export function updateTypeAndId( baseUrl: string, - tracingType: string, + annotationType: string, annotationId: string, ): string { // Update the baseUrl with a potentially new annotation id and or tracing type. @@ -136,7 +136,7 @@ export function updateTypeAndId( // dataset viewing only return baseUrl.replace( /^(.*\/annotations)\/(.*?)\/([^/?]*)(\/?.*)$/, - (all, base, type, id, rest) => `${base}/${tracingType}/${annotationId}${rest}`, + (all, base, type, id, rest) => `${base}/${annotationType}/${annotationId}${rest}`, ); } diff --git a/app/assets/javascripts/oxalis/model.js b/app/assets/javascripts/oxalis/model.js index 0499dcb634a..0b25da771b9 100644 --- a/app/assets/javascripts/oxalis/model.js +++ b/app/assets/javascripts/oxalis/model.js @@ -15,7 +15,7 @@ import type DataCube from "oxalis/model/bucket_data_handling/data_cube"; import DataLayer from "oxalis/model/data_layer"; import type LayerRenderingManager from "oxalis/model/bucket_data_handling/layer_rendering_manager"; import type PullQueue from "oxalis/model/bucket_data_handling/pullqueue"; -import Store, { type TraceOrViewCommand, type TracingTypeTracing } from "oxalis/store"; +import Store, { type TraceOrViewCommand, type AnnotationType } from "oxalis/store"; import * as Utils from "libs/utils"; import { initialize } from "./model_initialization"; @@ -30,13 +30,13 @@ export class OxalisModel { maximumDataTextureCountForLayer: number; async fetch( - tracingType: TracingTypeTracing, + annotationType: AnnotationType, initialCommandType: TraceOrViewCommand, initialFetch: boolean, versions?: Versions, ) { const initializationInformation = await initialize( - tracingType, + annotationType, initialCommandType, initialFetch, versions, diff --git a/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js b/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js index 6c4ba81165f..1096782eef6 100644 --- a/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js +++ b/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js @@ -85,7 +85,7 @@ export function getNmlName(state: OxalisState): string { if (tracing.name !== "") return `${tracing.name}.nml`; const datasetName = dataset.name; - const tracingType = task ? task.id : "explorational"; + const annotationTypeOrTaskId = task ? task.id : "explorational"; let userName = activeUser ? `${activeUser.firstName.slice(0, 1)}${activeUser.lastName}`.toLowerCase() : ""; @@ -93,7 +93,7 @@ export function getNmlName(state: OxalisState): string { userName = userName.replace(/ /g, "_"); const shortAnnotationId = tracing.annotationId.slice(-6); - return `${datasetName}__${tracingType}__${userName}__${shortAnnotationId}.nml`; + return `${datasetName}__${annotationTypeOrTaskId}__${userName}__${shortAnnotationId}.nml`; } export function serializeToNml( diff --git a/app/assets/javascripts/oxalis/model/reducers/reducer_helpers.js b/app/assets/javascripts/oxalis/model/reducers/reducer_helpers.js index 542f4754a08..283d0c39110 100644 --- a/app/assets/javascripts/oxalis/model/reducers/reducer_helpers.js +++ b/app/assets/javascripts/oxalis/model/reducers/reducer_helpers.js @@ -48,7 +48,7 @@ export function convertServerAnnotationToFrontendAnnotation(annotation: APIAnnot tags, description, name, - typ: tracingType, + typ: annotationType, tracingStore, meshes, } = annotation; @@ -63,7 +63,7 @@ export function convertServerAnnotationToFrontendAnnotation(annotation: APIAnnot tags, description, name, - tracingType, + annotationType, tracingStore, meshes, }; diff --git a/app/assets/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js b/app/assets/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js index 95cd36ce436..9da9422adff 100644 --- a/app/assets/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js +++ b/app/assets/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js @@ -49,7 +49,7 @@ export function generateTreeName(state: OxalisState, timestamp: number, treeId: } let prefix = "Tree"; - if (state.tracing.tracingType === "Explorational") { + if (state.tracing.annotationType === "Explorational") { // Get YYYY-MM-DD string const creationDate = new Date(timestamp).toJSON().slice(0, 10); prefix = `explorative_${creationDate}_${user}_`; diff --git a/app/assets/javascripts/oxalis/model/sagas/annotation_saga.js b/app/assets/javascripts/oxalis/model/sagas/annotation_saga.js index 50b3c321136..a36748190b8 100644 --- a/app/assets/javascripts/oxalis/model/sagas/annotation_saga.js +++ b/app/assets/javascripts/oxalis/model/sagas/annotation_saga.js @@ -21,7 +21,7 @@ export function* pushAnnotationUpdateAsync(): Saga { isPublic: tracing.isPublic, description: tracing.description, }; - yield* call(editAnnotation, tracing.annotationId, tracing.tracingType, editObject); + yield* call(editAnnotation, tracing.annotationId, tracing.annotationType, editObject); } function shouldDisplaySegmentationData(): boolean { diff --git a/app/assets/javascripts/oxalis/model_initialization.js b/app/assets/javascripts/oxalis/model_initialization.js index 53ed3805f43..086f3e131b3 100644 --- a/app/assets/javascripts/oxalis/model_initialization.js +++ b/app/assets/javascripts/oxalis/model_initialization.js @@ -58,7 +58,7 @@ import { setupGlobalMappingsObject } from "oxalis/model/bucket_data_handling/map import ConnectionInfo from "oxalis/model/data_connection_info"; import DataLayer from "oxalis/model/data_layer"; import ErrorHandling from "libs/error_handling"; -import Store, { type TraceOrViewCommand, type TracingTypeTracing } from "oxalis/store"; +import Store, { type TraceOrViewCommand, type AnnotationType } from "oxalis/store"; import Toast from "libs/toast"; import UrlManager, { type UrlManagerState } from "oxalis/controller/url_manager"; import * as Utils from "libs/utils"; @@ -72,7 +72,7 @@ type DataLayerCollection = { }; export async function initialize( - tracingType: TracingTypeTracing, + annotationType: AnnotationType, initialCommandType: TraceOrViewCommand, initialFetch: boolean, versions?: Versions, @@ -88,7 +88,7 @@ export async function initialize( let datasetId: APIDatasetId; if (initialCommandType.type === ControlModeEnum.TRACE) { const { annotationId } = initialCommandType; - annotation = await getAnnotationInformation(annotationId, tracingType); + annotation = await getAnnotationInformation(annotationId, annotationType); datasetId = { name: annotation.dataSetName, owningOrganization: annotation.organization }; if (!annotation.restrictions.allowAccess) { diff --git a/app/assets/javascripts/oxalis/store.js b/app/assets/javascripts/oxalis/store.js index 82f651908c9..1e4b9e83ce8 100644 --- a/app/assets/javascripts/oxalis/store.js +++ b/app/assets/javascripts/oxalis/store.js @@ -17,7 +17,7 @@ import type { APISettings, APITask, APITracingStore, - APITracingType, + APIAnnotationType, APIUser, MeshMetaData, } from "admin/api_flow_types"; @@ -131,7 +131,7 @@ export type DataStoreInfo = APIDataStore; export type TreeMap = { +[number]: Tree }; export type TemporaryMutableTreeMap = { [number]: Tree }; -export type TracingTypeTracing = APITracingType; +export type AnnotationType = APIAnnotationType; export type RestrictionsAndSettings = {| ...Restrictions, ...Settings |}; @@ -143,7 +143,7 @@ export type Annotation = {| +description: string, +name: string, +tracingStore: APITracingStore, - +tracingType: TracingTypeTracing, + +annotationType: AnnotationType, +meshes: Array, |}; @@ -396,7 +396,7 @@ const initialAnnotationInfo = { name: "localhost", url: "http://localhost:9000", }, - tracingType: "View", + annotationType: "View", meshes: [], }; diff --git a/app/assets/javascripts/oxalis/view/action-bar/merge_modal_view.js b/app/assets/javascripts/oxalis/view/action-bar/merge_modal_view.js index 3ea305bfbc8..62d47e6dd95 100644 --- a/app/assets/javascripts/oxalis/view/action-bar/merge_modal_view.js +++ b/app/assets/javascripts/oxalis/view/action-bar/merge_modal_view.js @@ -23,7 +23,7 @@ type ProjectInfo = { type StateProps = { annotationId: string, - tracingType: string, + annotationType: string, }; type Props = { @@ -122,7 +122,7 @@ class MergeModalView extends PureComponent { } else { const url = `/api/annotations/CompoundProject/${selectedProject}/merge/` + - `${this.props.tracingType}/${this.props.annotationId}`; + `${this.props.annotationType}/${this.props.annotationId}`; this.merge(url); } } @@ -145,7 +145,7 @@ class MergeModalView extends PureComponent { } else { const url = `/api/annotations/Explorational/${selectedExplorativeAnnotation}/merge/` + - `${this.props.tracingType}/${this.props.annotationId}`; + `${this.props.annotationType}/${this.props.annotationId}`; this.merge(url); } } @@ -254,7 +254,7 @@ class MergeModalView extends PureComponent { function mapStateToProps(state: OxalisState): StateProps { return { annotationId: state.tracing.annotationId, - tracingType: state.tracing.tracingType, + annotationType: state.tracing.annotationType, }; } diff --git a/app/assets/javascripts/oxalis/view/action-bar/tracing_actions_view.js b/app/assets/javascripts/oxalis/view/action-bar/tracing_actions_view.js index 4fee8029584..5f9ba4c5133 100644 --- a/app/assets/javascripts/oxalis/view/action-bar/tracing_actions_view.js +++ b/app/assets/javascripts/oxalis/view/action-bar/tracing_actions_view.js @@ -4,9 +4,9 @@ import { Button, Dropdown, Icon, Menu, Modal, Tooltip } from "antd"; import { connect } from "react-redux"; import * as React from "react"; -import type { APIUser, APITracingType } from "admin/api_flow_types"; +import type { APIUser, APIAnnotationType } from "admin/api_flow_types"; import { AsyncButton } from "components/async_clickables"; -import { copyAnnotationToUserAccount, finishAnnotation } from "admin/admin_rest_api"; +import { copyAnnotationToUserAccount, finishAnnotation, downloadNml } from "admin/admin_rest_api"; import { mapLayoutKeysToLanguage } from "oxalis/view/layouting/default_layout_configs"; import { setVersionRestoreVisibilityAction } from "oxalis/model/actions/ui_actions"; import { undoAction, redoAction } from "oxalis/model/actions/save_actions"; @@ -20,10 +20,11 @@ import Store, { type OxalisState, type RestrictionsAndSettings, type Task } from import UserScriptsModalView from "oxalis/view/action-bar/user_scripts_modal_view"; import api from "oxalis/api/internal_api"; import messages from "messages"; -import window, { location } from "libs/window"; +import { location } from "libs/window"; +import type { LayoutKeys } from "oxalis/view/layouting/default_layout_configs"; type StateProps = { - tracingType: APITracingType, + annotationType: APIAnnotationType, annotationId: string, restrictions: RestrictionsAndSettings, task: ?Task, @@ -40,17 +41,21 @@ type State = { isUserScriptsModalOpen: boolean, }; -type LayoutMenuProps = { +export type LayoutProps = { storedLayoutNamesForView: Array, - layoutKey: string, activeLayout: string, + layoutKey: LayoutKeys, + autoSaveLayouts: boolean, + setAutoSaveLayouts: boolean => void, + setCurrentLayout: string => void, + saveCurrentLayout: () => void, +}; + +type LayoutMenuProps = LayoutProps & { onResetLayout: () => void, onSelectLayout: string => void, onDeleteLayout: string => void, addNewLayout: () => void, - autoSaveLayouts: boolean, - setAutoSaveLayouts: boolean => void, - saveCurrentLayout: () => void, }; export const LayoutMenu = (props: LayoutMenuProps) => { @@ -65,6 +70,7 @@ export const LayoutMenu = (props: LayoutMenuProps) => { autoSaveLayouts, setAutoSaveLayouts, saveCurrentLayout, + setCurrentLayout, ...others } = props; const layoutMissingHelpTitle = ( @@ -190,7 +196,7 @@ class TracingActionsView extends React.PureComponent { handleCopyToAccount = async () => { const newAnnotation = await copyAnnotationToUserAccount( this.props.annotationId, - this.props.tracingType, + this.props.annotationType, ); location.href = `/annotations/Explorational/${newAnnotation.id}`; }; @@ -201,7 +207,7 @@ class TracingActionsView extends React.PureComponent { Modal.confirm({ title: messages["annotation.finish"], onOk: async () => { - await finishAnnotation(this.props.annotationId, this.props.tracingType); + await finishAnnotation(this.props.annotationId, this.props.annotationType); // Force page refresh location.href = "/dashboard"; }, @@ -217,15 +223,8 @@ class TracingActionsView extends React.PureComponent { }; handleDownload = async () => { - const win = window.open("about:blank", "_blank"); - win.document.body.innerHTML = messages["download.wait"]; await this.handleSave(); - - const downloadUrl = `/api/annotations/${this.props.tracingType}/${ - this.props.annotationId - }/download`; - win.location.href = downloadUrl; - win.document.body.innerHTML = messages["download.close_window"]; + downloadNml(this.props.annotationId, this.props.annotationType); }; handleFinishAndGetNextTask = async () => { @@ -349,14 +348,12 @@ class TracingActionsView extends React.PureComponent { ); } - if (restrictions.allowUpdate) { - elements.push( - - - Restore Older Version - , - ); - } + elements.push( + + + Restore Older Version + , + ); elements.push(this.props.layoutMenu); @@ -381,7 +378,7 @@ class TracingActionsView extends React.PureComponent { function mapStateToProps(state: OxalisState): StateProps { return { - tracingType: state.tracing.tracingType, + annotationType: state.tracing.annotationType, annotationId: state.tracing.annotationId, restrictions: state.tracing.restrictions, task: state.task, diff --git a/app/assets/javascripts/oxalis/view/action_bar_view.js b/app/assets/javascripts/oxalis/view/action_bar_view.js index 38280204a9f..9a9c0c98c30 100644 --- a/app/assets/javascripts/oxalis/view/action_bar_view.js +++ b/app/assets/javascripts/oxalis/view/action_bar_view.js @@ -3,7 +3,6 @@ import { Icon, Alert, Dropdown, Menu } from "antd"; import { connect } from "react-redux"; import * as React from "react"; -import type { LayoutKeys } from "oxalis/view/layouting/default_layout_configs"; import { layoutEmitter, deleteLayout, @@ -16,7 +15,10 @@ import ButtonComponent from "oxalis/view/components/button_component"; import Constants, { type ControlMode, ControlModeEnum, type Mode } from "oxalis/constants"; import DatasetPositionView from "oxalis/view/action-bar/dataset_position_view"; import Store, { type OxalisState } from "oxalis/store"; -import TracingActionsView, { LayoutMenu } from "oxalis/view/action-bar/tracing_actions_view"; +import TracingActionsView, { + LayoutMenu, + type LayoutProps, +} from "oxalis/view/action-bar/tracing_actions_view"; import ViewModesView from "oxalis/view/action-bar/view_modes_view"; import VolumeActionsView from "oxalis/view/action-bar/volume_actions_view"; @@ -37,15 +39,7 @@ type StateProps = { }; type Props = StateProps & { - layoutProps: { - storedLayoutNamesForView: Array, - activeLayout: string, - layoutKey: LayoutKeys, - autoSaveLayouts: boolean, - setAutoSaveLayouts: boolean => void, - setCurrentLayout: string => void, - saveCurrentLayout: () => void, - }, + layoutProps: LayoutProps, }; type State = { diff --git a/app/assets/javascripts/oxalis/view/layouting/tracing_layout_view.js b/app/assets/javascripts/oxalis/view/layouting/tracing_layout_view.js index b65624257e5..d9c6a651d52 100644 --- a/app/assets/javascripts/oxalis/view/layouting/tracing_layout_view.js +++ b/app/assets/javascripts/oxalis/view/layouting/tracing_layout_view.js @@ -11,7 +11,7 @@ import classNames from "classnames"; import type { Dispatch } from "redux"; import { ArbitraryViewport, type Mode, OrthoViews } from "oxalis/constants"; -import type { OxalisState, TracingTypeTracing, TraceOrViewCommand } from "oxalis/store"; +import type { OxalisState, AnnotationType, TraceOrViewCommand } from "oxalis/store"; import { updateUserSettingAction } from "oxalis/model/actions/settings_actions"; import AbstractTreeTabView from "oxalis/view/right-menu/abstract_tree_tab_view"; import ActionBarView from "oxalis/view/action_bar_view"; @@ -50,7 +50,7 @@ type StateProps = { }; type Props = StateProps & { - initialTracingType: TracingTypeTracing, + initialAnnotationType: AnnotationType, initialCommandType: TraceOrViewCommand, setAutoSaveLayouts: boolean => void, }; @@ -134,13 +134,13 @@ class TracingLayoutView extends React.PureComponent { render() { const layoutType = determineLayout(this.props.initialCommandType.type, this.props.viewMode); const currentLayoutNames = this.getLayoutNamesFromCurrentView(layoutType); - const { displayScalebars, isDatasetOnScratchVolume } = this.props; + const { displayScalebars, isDatasetOnScratchVolume, isUpdateTracingAllowed } = this.props; const headerClassName = classNames({ construction: isDatasetOnScratchVolume }); return ( @@ -234,7 +234,7 @@ class TracingLayoutView extends React.PureComponent {
{this.props.showVersionRestore ? ( - + ) : null} diff --git a/app/assets/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js b/app/assets/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js index 56ebf95f92f..3c67da6909b 100644 --- a/app/assets/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js +++ b/app/assets/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js @@ -7,7 +7,7 @@ import { connect } from "react-redux"; import Markdown from "react-remarkable"; import React from "react"; -import { type APIDataset, APITracingTypeEnum } from "admin/api_flow_types"; +import { type APIDataset, APIAnnotationTypeEnum } from "admin/api_flow_types"; import { aggregateBoundingBox } from "libs/utils"; import { convertToHybridTracing } from "admin/admin_rest_api"; import { formatScale } from "libs/format_utils"; @@ -209,14 +209,14 @@ class DatasetInfoTabView extends React.PureComponent { let annotationTypeLabel; - const { tracingType, name } = this.props.tracing; + const { annotationType, name } = this.props.tracing; const tracingName = name || ""; if (this.props.task != null) { // In case we have a task display its id annotationTypeLabel = ( - {tracingType} : {this.props.task.id} + {annotationType} : {this.props.task.id} ); } else if (!this.props.tracing.restrictions.allowUpdate) { @@ -269,7 +269,8 @@ class DatasetInfoTabView extends React.PureComponent { const isVolume = this.props.tracing.volume != null; const isHybrid = isSkeleton && isVolume; const { allowUpdate } = this.props.tracing.restrictions; - const isExplorational = this.props.tracing.tracingType === APITracingTypeEnum.Explorational; + const isExplorational = + this.props.tracing.annotationType === APIAnnotationTypeEnum.Explorational; if (isHybrid) { return ( diff --git a/app/assets/javascripts/oxalis/view/version_entry.js b/app/assets/javascripts/oxalis/view/version_entry.js index 66c36cf1136..43d591ed672 100644 --- a/app/assets/javascripts/oxalis/view/version_entry.js +++ b/app/assets/javascripts/oxalis/view/version_entry.js @@ -153,6 +153,7 @@ function getDescriptionForBatch(actions: Array): Description type Props = { actions: Array, + allowUpdate: boolean, version: number, isNewest: boolean, isActive: boolean, @@ -163,6 +164,7 @@ type Props = { export default function VersionEntry({ actions, + allowUpdate, version, isNewest, isActive, @@ -182,7 +184,7 @@ export default function VersionEntry({ type="primary" onClick={() => onRestoreVersion(version)} > - Restore + {allowUpdate ? "Restore" : "Download"} ); const { description, type } = getDescriptionForBatch(actions); diff --git a/app/assets/javascripts/oxalis/view/version_entry_group.js b/app/assets/javascripts/oxalis/view/version_entry_group.js index 58b4315bac1..0d55ff72636 100644 --- a/app/assets/javascripts/oxalis/view/version_entry_group.js +++ b/app/assets/javascripts/oxalis/view/version_entry_group.js @@ -9,6 +9,7 @@ import VersionEntry from "oxalis/view/version_entry"; type Props = { batches: Array, + allowUpdate: boolean, newestVersion: number, activeVersion: number, onRestoreVersion: number => Promise, @@ -33,6 +34,7 @@ export default class VersionEntryGroup extends React.Component { render() { const { batches, + allowUpdate, newestVersion, activeVersion, onRestoreVersion, @@ -74,6 +76,7 @@ export default class VersionEntryGroup extends React.Component { {this.state.expanded || !containsMultipleBatches ? batches.map(batch => ( { } handleRestoreVersion = async (version: number) => { - Store.dispatch(setVersionNumberAction(this.getNewestVersion(), this.props.tracingType)); - Store.dispatch(pushSaveQueueAction([revertToVersion(version)], this.props.tracingType)); - await Model.save(); - Store.dispatch(setVersionRestoreVisibilityAction(false)); - Store.dispatch(setAnnotationAllowUpdateAction(true)); + if (this.props.allowUpdate) { + Store.dispatch(setVersionNumberAction(this.getNewestVersion(), this.props.tracingType)); + Store.dispatch(pushSaveQueueAction([revertToVersion(version)], this.props.tracingType)); + await Model.save(); + Store.dispatch(setVersionRestoreVisibilityAction(false)); + Store.dispatch(setAnnotationAllowUpdateAction(true)); + } else { + const { annotationType, annotationId } = Store.getState().tracing; + downloadNml(annotationId, annotationType, { [this.props.tracingType]: version }); + } }; handlePreviewVersion = (version: number) => previewVersion({ [this.props.tracingType]: version }); @@ -140,6 +146,7 @@ class VersionList extends React.Component { ) : ( { state = { activeTracingType: this.props.tracing.skeleton != null ? "skeleton" : "volume", + // Remember whether the tracing could originally be updated + initialAllowUpdate: this.props.allowUpdate, }; + componentWillUnmount() { + Store.dispatch(setAnnotationAllowUpdateAction(this.state.initialAllowUpdate)); + } + handleClose = async () => { // This will load the newest version of both skeleton and volume tracings await previewVersion(); Store.dispatch(setVersionRestoreVisibilityAction(false)); - Store.dispatch(setAnnotationAllowUpdateAction(true)); + Store.dispatch(setAnnotationAllowUpdateAction(this.state.initialAllowUpdate)); }; onChangeTab = (activeKey: "skeleton" | "volume") => { @@ -73,7 +84,11 @@ class VersionView extends React.Component { {this.props.tracing.skeleton != null ? ( - + ) : null} {this.props.tracing.volume != null ? ( @@ -84,7 +99,11 @@ class VersionView extends React.Component { message="Volume versioning has been disabled for this instance. Please contact an administrator." /> ) : ( - + )} ) : null} @@ -95,7 +114,7 @@ class VersionView extends React.Component { } } -function mapStateToProps(state: OxalisState): Props { +function mapStateToProps(state: OxalisState): StateProps { return { tracing: state.tracing, }; diff --git a/app/assets/javascripts/router.js b/app/assets/javascripts/router.js index b62bc614355..66d7ffd4779 100644 --- a/app/assets/javascripts/router.js +++ b/app/assets/javascripts/router.js @@ -7,7 +7,7 @@ import Enum from "Enumjs"; import React from "react"; import createBrowserHistory from "history/createBrowserHistory"; -import { APITracingTypeEnum, type APIUser } from "admin/api_flow_types"; +import { APIAnnotationTypeEnum, type APIUser } from "admin/api_flow_types"; import { ControlModeEnum } from "oxalis/constants"; import { Imprint, Privacy } from "components/legal"; import type { OxalisState } from "oxalis/store"; @@ -88,12 +88,12 @@ function PageNotFoundView() { class ReactRouter extends React.Component { tracingView = ({ match }: ContextRouter) => { - const tracingType = Enum.coalesce(APITracingTypeEnum, match.params.type); + const annotationType = Enum.coalesce(APIAnnotationTypeEnum, match.params.type); - if (tracingType != null) { + if (annotationType != null) { return ( { tracingViewMode = ({ match }: ContextRouter) => ( { try { const annotationInformation = await getAnnotationInformation( match.params.id || "", - Enum.coalesce(APITracingTypeEnum, match.params.type) || - APITracingTypeEnum.Explorational, + Enum.coalesce(APIAnnotationTypeEnum, match.params.type) || + APIAnnotationTypeEnum.Explorational, ); return annotationInformation.isPublic; } catch (ex) { diff --git a/app/assets/javascripts/test/backend-snapshot-tests/annotations.e2e.js b/app/assets/javascripts/test/backend-snapshot-tests/annotations.e2e.js index f9e3249fc38..881a96f6aad 100644 --- a/app/assets/javascripts/test/backend-snapshot-tests/annotations.e2e.js +++ b/app/assets/javascripts/test/backend-snapshot-tests/annotations.e2e.js @@ -1,6 +1,6 @@ /* eslint import/no-extraneous-dependencies: ["error", {"peerDependencies": true}] */ // @flow -import { APITracingTypeEnum } from "admin/api_flow_types"; +import { APIAnnotationTypeEnum } from "admin/api_flow_types"; import { createTreeMapFromTreeArray } from "oxalis/model/reducers/skeletontracing_reducer_helpers"; import { diffTrees } from "oxalis/model/sagas/skeletontracing_saga"; import { @@ -33,7 +33,7 @@ test("getAnnotationInformation()", async t => { const annotationId = "570b9ff12a7c0e980056fe8f"; const annotation = await api.getAnnotationInformation( annotationId, - APITracingTypeEnum.Explorational, + APIAnnotationTypeEnum.Explorational, ); t.is(annotation.id, annotationId); writeFlowCheckingFile(annotation, "annotation", "APIAnnotation"); @@ -42,11 +42,11 @@ test("getAnnotationInformation()", async t => { test.serial("finishAnnotation() and reOpenAnnotation() for task", async t => { const annotationId = "78135c192faeb34c0081c05d"; - const finishedAnnotation = await api.finishAnnotation(annotationId, APITracingTypeEnum.Task); + const finishedAnnotation = await api.finishAnnotation(annotationId, APIAnnotationTypeEnum.Task); t.is(finishedAnnotation.state, "Finished"); t.snapshot(finishedAnnotation, { id: "annotations-finishAnnotation" }); - const reopenedAnnotation = await api.reOpenAnnotation(annotationId, APITracingTypeEnum.Task); + const reopenedAnnotation = await api.reOpenAnnotation(annotationId, APIAnnotationTypeEnum.Task); t.is(reopenedAnnotation.state, "Active"); t.snapshot(reopenedAnnotation, { id: "annotations-reOpenAnnotation" }); @@ -56,14 +56,14 @@ test.serial("finishAnnotation() and reOpenAnnotation() for explorational", async const annotationId = "68135c192faeb34c0081c05d"; const finishedAnnotation = await api.finishAnnotation( annotationId, - APITracingTypeEnum.Explorational, + APIAnnotationTypeEnum.Explorational, ); t.is(finishedAnnotation.state, "Finished"); t.snapshot(finishedAnnotation, { id: "annotations-finishAnnotation-explorational" }); const reopenedAnnotation = await api.reOpenAnnotation( annotationId, - APITracingTypeEnum.Explorational, + APIAnnotationTypeEnum.Explorational, ); t.is(reopenedAnnotation.state, "Active"); @@ -74,7 +74,7 @@ test.serial("editAnnotation()", async t => { const annotationId = "68135c192faeb34c0081c05d"; const originalAnnotation = await api.getAnnotationInformation( annotationId, - APITracingTypeEnum.Explorational, + APIAnnotationTypeEnum.Explorational, ); const { name, isPublic, description } = originalAnnotation; @@ -82,14 +82,14 @@ test.serial("editAnnotation()", async t => { const newIsPublic = !isPublic; const newDescription = "new description"; - await api.editAnnotation(annotationId, APITracingTypeEnum.Explorational, { + await api.editAnnotation(annotationId, APIAnnotationTypeEnum.Explorational, { name: newName, isPublic: newIsPublic, description: newDescription, }); const editedAnnotation = await api.getAnnotationInformation( annotationId, - APITracingTypeEnum.Explorational, + APIAnnotationTypeEnum.Explorational, ); t.is(editedAnnotation.name, newName); @@ -99,7 +99,7 @@ test.serial("editAnnotation()", async t => { t.is(editedAnnotation.tracing.skeleton, "ae417175-f7bb-4a34-8187-d9c3b50143af"); t.snapshot(replaceVolatileValues(editedAnnotation), { id: "annotations-editAnnotation" }); - await api.editAnnotation(annotationId, APITracingTypeEnum.Explorational, { + await api.editAnnotation(annotationId, APIAnnotationTypeEnum.Explorational, { name, isPublic, description, @@ -112,7 +112,7 @@ test.serial("finishAllAnnotations()", async t => { await api.finishAllAnnotations(annotationIds); const finishedAnnotations = await Promise.all( - annotationIds.map(id => api.getAnnotationInformation(id, APITracingTypeEnum.Explorational)), + annotationIds.map(id => api.getAnnotationInformation(id, APIAnnotationTypeEnum.Explorational)), ); t.is(finishedAnnotations.length, 2); @@ -120,7 +120,7 @@ test.serial("finishAllAnnotations()", async t => { t.is(annotation.state, "Finished"); }); - await Promise.all(annotationIds.map(id => api.reOpenAnnotation(id, APITracingTypeEnum.Task))); + await Promise.all(annotationIds.map(id => api.reOpenAnnotation(id, APIAnnotationTypeEnum.Task))); }); test.serial("createExplorational() and finishAnnotation()", async t => { @@ -130,11 +130,11 @@ test.serial("createExplorational() and finishAnnotation()", async t => { id: "annotations-createExplorational", }); - await api.finishAnnotation(createdExplorational.id, APITracingTypeEnum.Explorational); + await api.finishAnnotation(createdExplorational.id, APIAnnotationTypeEnum.Explorational); const finishedAnnotation = await api.getAnnotationInformation( createdExplorational.id, - APITracingTypeEnum.Explorational, + APIAnnotationTypeEnum.Explorational, ); t.is(finishedAnnotation.state, "Finished"); }); diff --git a/app/assets/javascripts/test/controller/url_manager.spec.js b/app/assets/javascripts/test/controller/url_manager.spec.js index 084f93b1a0e..b419c1c3f2b 100644 --- a/app/assets/javascripts/test/controller/url_manager.spec.js +++ b/app/assets/javascripts/test/controller/url_manager.spec.js @@ -9,19 +9,19 @@ const { updateTypeAndId } = mockRequire.reRequire("oxalis/controller/url_manager test("UrlManager should replace tracing in url", t => { t.is( updateTypeAndId( - "abc/def/annotations/tracingType/annotationId", - "newTracingType", + "abc/def/annotations/annotationType/annotationId", + "newAnnotationType", "newAnnotationId", ), - "abc/def/annotations/newTracingType/newAnnotationId", + "abc/def/annotations/newAnnotationType/newAnnotationId", ); t.is( updateTypeAndId( - "abc/def/annotations/tracingType/annotationId/readOnly", - "newTracingType", + "abc/def/annotations/annotationType/annotationId/readOnly", + "newAnnotationType", "newAnnotationId", ), - "abc/def/annotations/newTracingType/newAnnotationId/readOnly", + "abc/def/annotations/newAnnotationType/newAnnotationId/readOnly", ); }); diff --git a/app/assets/javascripts/test/helpers/apiHelpers.js b/app/assets/javascripts/test/helpers/apiHelpers.js index 77dd01f5606..5166430a64d 100644 --- a/app/assets/javascripts/test/helpers/apiHelpers.js +++ b/app/assets/javascripts/test/helpers/apiHelpers.js @@ -103,7 +103,7 @@ const modelData = { }, }; -const TRACING_TYPE = "tracingTypeValue"; +const ANNOTATION_TYPE = "annotationTypeValue"; const ANNOTATION_ID = "annotationIdValue"; let counter = 0; @@ -120,7 +120,7 @@ export function __setupOxalis(t, mode, apiVersion) { const ANNOTATION = modelData[mode].annotation; Request.receiveJSON - .withArgs(`/api/annotations/${TRACING_TYPE}/${ANNOTATION_ID}/info`) + .withArgs(`/api/annotations/${ANNOTATION_TYPE}/${ANNOTATION_ID}/info`) .returns(Promise.resolve(_.cloneDeep(ANNOTATION))); const datasetClone = _.cloneDeep(DATASET); @@ -139,7 +139,7 @@ export function __setupOxalis(t, mode, apiVersion) { Request.receiveJSON.returns(Promise.resolve({})); return Model.fetch( - TRACING_TYPE, + ANNOTATION_TYPE, { annotationId: ANNOTATION_ID, type: ControlModeEnum.TRACE }, true, ) diff --git a/app/assets/javascripts/test/libs/nml.spec.js b/app/assets/javascripts/test/libs/nml.spec.js index 227c5cef782..fda770c26d2 100644 --- a/app/assets/javascripts/test/libs/nml.spec.js +++ b/app/assets/javascripts/test/libs/nml.spec.js @@ -88,7 +88,7 @@ const tracing = { groupId: 2, }, }, - tracingType: "Explorational", + annotationType: "Explorational", treeGroups: [ { groupId: 1, diff --git a/app/assets/javascripts/test/model/model.spec.js b/app/assets/javascripts/test/model/model.spec.js index 2b2a766320b..9a711892059 100644 --- a/app/assets/javascripts/test/model/model.spec.js +++ b/app/assets/javascripts/test/model/model.spec.js @@ -48,7 +48,7 @@ mockRequire("oxalis/model/bucket_data_handling/wkstore_adapter", {}); const Model = mockRequire.reRequire("../../oxalis/model").OxalisModel; const { HANDLED_ERROR } = mockRequire.reRequire("../../oxalis/model_initialization"); -const TRACING_TYPE = "tracingTypeValue"; +const ANNOTATION_TYPE = "annotationTypeValue"; const ANNOTATION_ID = "annotationIdValue"; test.beforeEach(t => { @@ -57,7 +57,7 @@ test.beforeEach(t => { model.state = { position: [1, 2, 3] }; Request.receiveJSON - .withArgs(`/api/annotations/${TRACING_TYPE}/${ANNOTATION_ID}/info`) + .withArgs(`/api/annotations/${ANNOTATION_TYPE}/${ANNOTATION_ID}/info`) .returns(Promise.resolve(_.cloneDeep(ANNOTATION))); Request.receiveJSON .withArgs(`/api/datasets/${ANNOTATION.dataSetName}`) @@ -95,7 +95,7 @@ test("Model Initialization: should throw a model.HANDLED_ERROR for missing data .returns(Promise.resolve(_.cloneDeep(datasetObject))); return model - .fetch(TRACING_TYPE, ANNOTATION_ID, "VIEW", true) + .fetch(ANNOTATION_TYPE, ANNOTATION_ID, "VIEW", true) .then(() => { t.fail("Promise should not have been resolved."); }) @@ -114,7 +114,7 @@ test("Model Initialization: should throw an Error on unexpected failure", t => { return model .fetch( - TRACING_TYPE, + ANNOTATION_TYPE, { name: ANNOTATION.dataSetName, owningOrganization: "Connectomics Department", type: "VIEW" }, true, ) diff --git a/app/assets/javascripts/test/reducers/skeletontracing_reducer.spec.js b/app/assets/javascripts/test/reducers/skeletontracing_reducer.spec.js index 8a0373745e0..ad259403d72 100644 --- a/app/assets/javascripts/test/reducers/skeletontracing_reducer.spec.js +++ b/app/assets/javascripts/test/reducers/skeletontracing_reducer.spec.js @@ -54,7 +54,7 @@ const initialState = { allowAccess: true, allowDownload: true, }, - tracingType: "Explorational", + annotationType: "Explorational", skeleton: { type: "skeleton", trees: { diff --git a/app/assets/javascripts/test/reducers/volumetracing_reducer.spec.js b/app/assets/javascripts/test/reducers/volumetracing_reducer.spec.js index d3170949568..4159b5c2240 100644 --- a/app/assets/javascripts/test/reducers/volumetracing_reducer.spec.js +++ b/app/assets/javascripts/test/reducers/volumetracing_reducer.spec.js @@ -37,7 +37,7 @@ function getVolumeTracing(tracing: Tracing): Maybe { const initialState = update(defaultState, { tracing: { - tracingType: { $set: "Explorational" }, + annotationType: { $set: "Explorational" }, name: { $set: "" }, restrictions: { $set: { diff --git a/app/assets/javascripts/test/sagas/save_saga.spec.js b/app/assets/javascripts/test/sagas/save_saga.spec.js index 18bb6e24766..ac9e39d788c 100644 --- a/app/assets/javascripts/test/sagas/save_saga.spec.js +++ b/app/assets/javascripts/test/sagas/save_saga.spec.js @@ -58,7 +58,7 @@ const initialState = { color: [23, 23, 23], }, }, - tracingType: "Explorational", + annotationType: "Explorational", name: "", activeTreeId: 1, activeNodeId: null, diff --git a/app/assets/javascripts/test/sagas/skeletontracing_saga.spec.js b/app/assets/javascripts/test/sagas/skeletontracing_saga.spec.js index b7ab095114c..18c60279e13 100644 --- a/app/assets/javascripts/test/sagas/skeletontracing_saga.spec.js +++ b/app/assets/javascripts/test/sagas/skeletontracing_saga.spec.js @@ -71,7 +71,7 @@ const initialState = { allowAccess: true, allowDownload: true, }, - tracingType: "Explorational", + annotationType: "Explorational", name: "", skeleton: { type: "skeleton", diff --git a/app/assets/javascripts/test/sagas/volumetracing_saga.spec.js b/app/assets/javascripts/test/sagas/volumetracing_saga.spec.js index 0218bc923bf..ca5d0e6b87a 100644 --- a/app/assets/javascripts/test/sagas/volumetracing_saga.spec.js +++ b/app/assets/javascripts/test/sagas/volumetracing_saga.spec.js @@ -37,7 +37,7 @@ const { defaultState } = require("oxalis/store"); const volumeTracing = { type: "volume", - tracingType: "Explorational", + annotationType: "Explorational", name: "", activeTool: VolumeToolEnum.TRACE, activeCellId: 0, diff --git a/app/controllers/AnnotationIOController.scala b/app/controllers/AnnotationIOController.scala index 2437b3cdcb4..a6ba444218a 100755 --- a/app/controllers/AnnotationIOController.scala +++ b/app/controllers/AnnotationIOController.scala @@ -185,28 +185,32 @@ class AnnotationIOController @Inject()(nmlWriter: NmlWriter, ) } - def download(typ: String, id: String) = sil.SecuredAction.async { implicit request => - logger.trace(s"Requested download for annotation: $typ/$id") - for { - identifier <- AnnotationIdentifier.parse(typ, id) - result <- identifier.annotationType match { - case AnnotationType.View => Fox.failure("Cannot download View annotation") - case AnnotationType.CompoundProject => downloadProject(id, request.identity) - case AnnotationType.CompoundTask => downloadTask(id, request.identity) - case AnnotationType.CompoundTaskType => downloadTaskType(id, request.identity) - case _ => downloadExplorational(id, typ, request.identity) - } - } yield result - } + def download(typ: String, id: String, skeletonVersion: Option[Long], volumeVersion: Option[Long]) = + sil.SecuredAction.async { implicit request => + logger.trace(s"Requested download for annotation: $typ/$id") + for { + identifier <- AnnotationIdentifier.parse(typ, id) + result <- identifier.annotationType match { + case AnnotationType.View => Fox.failure("Cannot download View annotation") + case AnnotationType.CompoundProject => downloadProject(id, request.identity) + case AnnotationType.CompoundTask => downloadTask(id, request.identity) + case AnnotationType.CompoundTaskType => downloadTaskType(id, request.identity) + case _ => downloadExplorational(id, typ, request.identity, skeletonVersion, volumeVersion) + } + } yield result + } - def downloadExplorational(annotationId: String, typ: String, issuingUser: User)(implicit ctx: DBAccessContext, - m: MessagesProvider) = { + def downloadExplorational(annotationId: String, + typ: String, + issuingUser: User, + skeletonVersion: Option[Long], + volumeVersion: Option[Long])(implicit ctx: DBAccessContext, m: MessagesProvider) = { def skeletonToDownloadStream(dataSet: DataSet, annotation: Annotation, name: String, organizationName: String) = for { tracingStoreClient <- tracingStoreService.clientFor(dataSet) skeletonTracingId <- annotation.skeletonTracingId.toFox - tracing <- tracingStoreClient.getSkeletonTracing(skeletonTracingId) + tracing <- tracingStoreClient.getSkeletonTracing(skeletonTracingId, skeletonVersion) user <- userService.findOneById(annotation._user, useCache = true) taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne) } yield { @@ -227,8 +231,10 @@ class AnnotationIOController @Inject()(nmlWriter: NmlWriter, for { tracingStoreClient <- tracingStoreService.clientFor(dataSet) volumeTracingId <- annotation.volumeTracingId.toFox - (volumeTracing, data: Source[ByteString, _]) <- tracingStoreClient.getVolumeTracing(volumeTracingId) - skeletonTracingOpt <- Fox.runOptional(annotation.skeletonTracingId)(tracingStoreClient.getSkeletonTracing) + (volumeTracing, data: Source[ByteString, _]) <- tracingStoreClient.getVolumeTracing(volumeTracingId, + volumeVersion) + skeletonTracingOpt <- Fox.runOptional(annotation.skeletonTracingId)(skeletonId => + tracingStoreClient.getSkeletonTracing(skeletonId, skeletonVersion)) user <- userService.findOneById(annotation._user, useCache = true) taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne) } yield { diff --git a/app/models/annotation/TracingStoreRpcClient.scala b/app/models/annotation/TracingStoreRpcClient.scala index 6b924197948..ec823e3558a 100644 --- a/app/models/annotation/TracingStoreRpcClient.scala +++ b/app/models/annotation/TracingStoreRpcClient.scala @@ -25,10 +25,11 @@ class TracingStoreRpcClient(tracingStore: TracingStore, dataSet: DataSet, rpc: R def baseInfo = s"Dataset: ${dataSet.name} Tracingstore: ${tracingStore.url}" - def getSkeletonTracing(tracingId: String): Fox[SkeletonTracing] = { + def getSkeletonTracing(tracingId: String, version: Option[Long]): Fox[SkeletonTracing] = { logger.debug("Called to get SkeletonTracing." + baseInfo) rpc(s"${tracingStore.url}/tracings/skeleton/${tracingId}") .addQueryString("token" -> TracingStoreRpcClient.webKnossosToken) + .addQueryStringOptional("version", version.map(_.toString)) .getWithProtoResponse[SkeletonTracing](SkeletonTracing) } @@ -104,14 +105,16 @@ class TracingStoreRpcClient(tracingStore: TracingStore, dataSet: DataSet, rpc: R } } - def getVolumeTracing(tracingId: String): Fox[(VolumeTracing, Source[ByteString, _])] = { + def getVolumeTracing(tracingId: String, version: Option[Long] = None): Fox[(VolumeTracing, Source[ByteString, _])] = { logger.debug("Called to get VolumeTracing." + baseInfo) for { tracing <- rpc(s"${tracingStore.url}/tracings/volume/${tracingId}") .addQueryString("token" -> TracingStoreRpcClient.webKnossosToken) + .addQueryStringOptional("version", version.map(_.toString)) .getWithProtoResponse[VolumeTracing](VolumeTracing) data <- rpc(s"${tracingStore.url}/tracings/volume/${tracingId}/allData") .addQueryString("token" -> TracingStoreRpcClient.webKnossosToken) + .addQueryStringOptional("version", version.map(_.toString)) .getStream } yield { (tracing, data) diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index 53522cf5be2..a5bf57dd9c5 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -103,7 +103,7 @@ GET /annotations/:typ/:id/info PATCH /annotations/:typ/:id/makeHybrid controllers.AnnotationController.makeHybrid(typ: String, id: String) DELETE /annotations/:typ/:id controllers.AnnotationController.cancel(typ: String, id: String) POST /annotations/:typ/:id/merge/:mergedTyp/:mergedId controllers.AnnotationController.merge(typ: String, id: String, mergedTyp: String, mergedId: String) -GET /annotations/:typ/:id/download controllers.AnnotationIOController.download(typ: String, id: String) +GET /annotations/:typ/:id/download controllers.AnnotationIOController.download(typ: String, id: String, skeletonVersion: Option[Long], volumeVersion: Option[Long]) GET /annotations/:typ/:id/loggedTime controllers.AnnotationController.loggedTime(typ: String, id: String)