From 5091391c3e1506ffd04074e616669edf978a0369 Mon Sep 17 00:00:00 2001 From: Daniel Werner Date: Thu, 6 Sep 2018 15:21:19 +0200 Subject: [PATCH 01/13] first working version of tracing version restore --- .../javascripts/admin/admin_rest_api.js | 23 ++- app/assets/javascripts/messages.js | 3 +- .../javascripts/oxalis/api/api_latest.js | 3 +- app/assets/javascripts/oxalis/model.js | 2 + .../model/actions/annotation_actions.js | 13 ++ .../oxalis/model/actions/ui_actions.js | 12 +- .../model/reducers/annotation_reducer.js | 7 +- .../oxalis/model/reducers/ui_reducer.js | 8 + .../oxalis/model/sagas/update_actions.js | 15 ++ .../oxalis/model_initialization.js | 5 +- app/assets/javascripts/oxalis/store.js | 2 + .../view/action-bar/tracing_actions_view.js | 14 ++ .../oxalis/view/tracing_layout_view.js | 8 + .../javascripts/oxalis/view/version_view.js | 172 ++++++++++++++++++ .../stylesheets/trace_view/_tracing_view.less | 14 ++ 15 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/oxalis/view/version_view.js diff --git a/app/assets/javascripts/admin/admin_rest_api.js b/app/assets/javascripts/admin/admin_rest_api.js index 0589120c3b1..b87e77bb094 100644 --- a/app/assets/javascripts/admin/admin_rest_api.js +++ b/app/assets/javascripts/admin/admin_rest_api.js @@ -532,10 +532,11 @@ export function createExplorational( export async function getTracingForAnnotations( annotation: APIAnnotationType, + version?: number, ): Promise { const [_skeleton, _volume] = await Promise.all([ - getTracingForAnnotationType(annotation, "skeleton"), - getTracingForAnnotationType(annotation, "volume"), + getTracingForAnnotationType(annotation, "skeleton", version), + getTracingForAnnotationType(annotation, "volume", version), ]); const skeleton = ((_skeleton: any): ?ServerSkeletonTracingType); @@ -550,14 +551,18 @@ export async function getTracingForAnnotations( export async function getTracingForAnnotationType( annotation: APIAnnotationType, tracingType: "skeleton" | "volume", + version?: number, ): Promise { const tracingId = annotation.tracing[tracingType]; if (!tracingId) { return null; } + const possibleVersionString = version != null ? `&version=${version}` : ""; const tracingArrayBuffer = await doWithToken(token => Request.receiveArraybuffer( - `${annotation.dataStore.url}/data/tracings/${tracingType}/${tracingId}?token=${token}`, + `${ + annotation.dataStore.url + }/data/tracings/${tracingType}/${tracingId}?token=${token}${possibleVersionString}`, { headers: { Accept: "application/x-protobuf" } }, ), ); @@ -568,6 +573,18 @@ export async function getTracingForAnnotationType( return tracing; } +export function getUpdateActionLog( + dataStoreUrl: string, + tracingId: string, + tracingType: "skeleton" | "volume", +): Promise<*> { + return doWithToken(token => + Request.receiveJSON( + `${dataStoreUrl}/data/tracings/${tracingType}/${tracingId}/updateActionLog?token=${token}`, + ), + ); +} + // ### Datasets export async function getDatasets(): Promise> { const datasets = await Request.receiveJSON("/api/datasets"); diff --git a/app/assets/javascripts/messages.js b/app/assets/javascripts/messages.js index 845bce88fea..4965f4b71c7 100644 --- a/app/assets/javascripts/messages.js +++ b/app/assets/javascripts/messages.js @@ -21,7 +21,8 @@ In order to restore the current window, a reload is necessary.`, "save.leave_page_unfinished": "WARNING: You have unsaved progress that may be lost when hitting OK. Please click cancel, wait until the progress is saved and the save button displays a checkmark before leaving the page..", "save.failed": "Failed to save tracing. Retrying.", - "undo.no_undo": "There is no action that could be undone.", + "undo.no_undo": + "There is no action that could be undone. However, if you want to restore an earlier version of this tracing, use the 'Restore Older Version' functionality in the dropdown next to the 'Save' button.", "undo.no_redo": "There is no action that could be redone.", "download.wait": "Please wait...", "download.close_window": "You may close this window after the download has started.", diff --git a/app/assets/javascripts/oxalis/api/api_latest.js b/app/assets/javascripts/oxalis/api/api_latest.js index 061d769fff9..62f8f36071a 100644 --- a/app/assets/javascripts/oxalis/api/api_latest.js +++ b/app/assets/javascripts/oxalis/api/api_latest.js @@ -412,10 +412,11 @@ class TracingApi { newTracingType: TracingTypeTracingType, newAnnotationId: string, newControlMode: ControlModeType, + version?: number, ) { Store.dispatch(restartSagaAction()); UrlManager.reset(); - await Model.fetch(newTracingType, newAnnotationId, newControlMode, false); + await Model.fetch(newTracingType, newAnnotationId, newControlMode, false, version); Store.dispatch(discardSaveQueuesAction()); Store.dispatch(wkReadyAction()); UrlManager.updateUnthrottled(); diff --git a/app/assets/javascripts/oxalis/model.js b/app/assets/javascripts/oxalis/model.js index 904590a71fe..bc170364323 100644 --- a/app/assets/javascripts/oxalis/model.js +++ b/app/assets/javascripts/oxalis/model.js @@ -31,12 +31,14 @@ export class OxalisModel { annotationIdOrDatasetName: string, controlMode: ControlModeType, initialFetch: boolean, + version?: number, ) { const initializationInformation = await initialize( tracingType, annotationIdOrDatasetName, controlMode, initialFetch, + version, ); if (initializationInformation) { // Only executed on initial fetch diff --git a/app/assets/javascripts/oxalis/model/actions/annotation_actions.js b/app/assets/javascripts/oxalis/model/actions/annotation_actions.js index 2ac175dfea6..436d33d794f 100644 --- a/app/assets/javascripts/oxalis/model/actions/annotation_actions.js +++ b/app/assets/javascripts/oxalis/model/actions/annotation_actions.js @@ -22,6 +22,11 @@ type SetAnnotationDescriptionActionType = { description: string, }; +type SetAnnotationAllowUpdateActionType = { + type: "SET_ANNOTATION_ALLOW_UPDATE", + allowUpdate: boolean, +}; + type SetUserBoundingBoxType = { type: "SET_USER_BOUNDING_BOX", userBoundingBox: ?BoundingBoxType, @@ -32,6 +37,7 @@ export type AnnotationActionTypes = | SetAnnotationNameActionType | SetAnnotationPubliceActionType | SetAnnotationDescriptionActionType + | SetAnnotationAllowUpdateActionType | SetUserBoundingBoxType; export const initializeAnnotationAction = ( @@ -58,6 +64,13 @@ export const setAnnotationDescriptionAction = ( description, }); +export const setAnnotationAllowUpdateAction = ( + allowUpdate: boolean, +): SetAnnotationAllowUpdateActionType => ({ + type: "SET_ANNOTATION_ALLOW_UPDATE", + allowUpdate, +}); + // Strictly speaking this is no annotation action but a tracing action, as the boundingBox is saved with // the tracing, hence no ANNOTATION in the action type. export const setUserBoundingBoxAction = ( diff --git a/app/assets/javascripts/oxalis/model/actions/ui_actions.js b/app/assets/javascripts/oxalis/model/actions/ui_actions.js index b1820bce0f4..e82c2063677 100644 --- a/app/assets/javascripts/oxalis/model/actions/ui_actions.js +++ b/app/assets/javascripts/oxalis/model/actions/ui_actions.js @@ -6,7 +6,12 @@ type SetDropzoneModalVisibilityActionType = { visible: boolean, }; -export type UiActionType = SetDropzoneModalVisibilityActionType; +type SetVersionRestoreModeActionType = { + type: "SET_VERSION_RESTORE_MODE_ACTION_TYPE", + active: boolean, +}; + +export type UiActionType = SetDropzoneModalVisibilityActionType | SetVersionRestoreModeActionType; export const setDropzoneModalVisibilityAction = ( visible: boolean, @@ -14,3 +19,8 @@ export const setDropzoneModalVisibilityAction = ( type: "SET_DROPZONE_MODAL_VISIBILITY_ACTION_TYPE", visible, }); + +export const setVersionRestoreModeAction = (active: boolean): SetVersionRestoreModeActionType => ({ + type: "SET_VERSION_RESTORE_MODE_ACTION_TYPE", + active, +}); diff --git a/app/assets/javascripts/oxalis/model/reducers/annotation_reducer.js b/app/assets/javascripts/oxalis/model/reducers/annotation_reducer.js index 132d375667d..973c4ffd2e8 100644 --- a/app/assets/javascripts/oxalis/model/reducers/annotation_reducer.js +++ b/app/assets/javascripts/oxalis/model/reducers/annotation_reducer.js @@ -1,7 +1,7 @@ // @flow import update from "immutability-helper"; -import { updateKey, type StateShape1 } from "oxalis/model/helpers/deep_update"; +import { updateKey, updateKey2, type StateShape1 } from "oxalis/model/helpers/deep_update"; import type { OxalisState } from "oxalis/store"; import type { ActionType } from "oxalis/model/actions/actions"; import { convertServerAnnotationToFrontendAnnotation } from "oxalis/model/reducers/reducer_helpers"; @@ -36,6 +36,11 @@ function AnnotationReducer(state: OxalisState, action: ActionType): OxalisState }); } + case "SET_ANNOTATION_ALLOW_UPDATE": { + const { allowUpdate } = action; + return updateKey2(state, "tracing", "restrictions", { allowUpdate }); + } + case "SET_USER_BOUNDING_BOX": { const updaterObject = { userBoundingBox: { diff --git a/app/assets/javascripts/oxalis/model/reducers/ui_reducer.js b/app/assets/javascripts/oxalis/model/reducers/ui_reducer.js index 1a4043adaad..49068623194 100644 --- a/app/assets/javascripts/oxalis/model/reducers/ui_reducer.js +++ b/app/assets/javascripts/oxalis/model/reducers/ui_reducer.js @@ -14,6 +14,14 @@ function UiReducer(state: OxalisState, action: ActionType): OxalisState { }); } + case "SET_VERSION_RESTORE_MODE_ACTION_TYPE": { + return update(state, { + uiInformation: { + isVersionRestoreActive: { $set: action.active }, + }, + }); + } + default: return state; } diff --git a/app/assets/javascripts/oxalis/model/sagas/update_actions.js b/app/assets/javascripts/oxalis/model/sagas/update_actions.js index 771e23937dd..f33ce7440ee 100644 --- a/app/assets/javascripts/oxalis/model/sagas/update_actions.js +++ b/app/assets/javascripts/oxalis/model/sagas/update_actions.js @@ -115,6 +115,12 @@ type UpdateTreeGroupsUpdateAction = { treeGroups: Array, }, }; +type RevertToVersionUpdateAction = { + name: "revertToVersion", + value: { + sourceVersion: number, + }, +}; type UpdateTracingUpdateAction = | UpdateSkeletonTracingUpdateAction | UpdateVolumeTracingUpdateAction; @@ -132,6 +138,7 @@ export type UpdateAction = | UpdateTracingUpdateAction | UpdateBucketUpdateAction | ToggleTreeUpdateAction + | RevertToVersionUpdateAction | UpdateTreeGroupsUpdateAction; export function createTree(tree: TreeType): UpdateTreeUpdateAction { @@ -300,3 +307,11 @@ export function updateTreeGroups(treeGroups: Array): UpdateTreeGr }, }; } +export function revertToVersion(version: number) { + return { + name: "revertToVersion", + value: { + sourceVersion: version, + }, + }; +} diff --git a/app/assets/javascripts/oxalis/model_initialization.js b/app/assets/javascripts/oxalis/model_initialization.js index e6d9a1e5abb..caaaa94919a 100644 --- a/app/assets/javascripts/oxalis/model_initialization.js +++ b/app/assets/javascripts/oxalis/model_initialization.js @@ -72,6 +72,7 @@ export async function initialize( annotationIdOrDatasetName: string, controlMode: ControlModeType, initialFetch: boolean, + version?: number, ): Promise { return Promise.all([ getDataset(datasetName, getSharingToken()), @@ -140,7 +143,7 @@ async function fetchParallel( // Fetch the actual tracing from the datastore, if there is an skeletonAnnotation // (Also see https://github.com/facebook/flow/issues/4936) // $FlowFixMe: Type inference with Promise.all seems to be a bit broken in flow - annotation ? getTracingForAnnotations(annotation) : null, + annotation ? getTracingForAnnotations(annotation, version) : null, ]); } diff --git a/app/assets/javascripts/oxalis/store.js b/app/assets/javascripts/oxalis/store.js index e9fd53fda28..99bd85f4280 100644 --- a/app/assets/javascripts/oxalis/store.js +++ b/app/assets/javascripts/oxalis/store.js @@ -326,6 +326,7 @@ export type ViewModeData = { type UiInformationType = { +showDropzoneModal: boolean, + +isVersionRestoreActive: boolean, }; export type OxalisState = {| @@ -492,6 +493,7 @@ export const defaultState: OxalisState = { activeUser: null, uiInformation: { showDropzoneModal: false, + isVersionRestoreActive: false, }, }; 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 64194ee1dc0..d605e175681 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 @@ -13,6 +13,7 @@ import ButtonComponent from "oxalis/view/components/button_component"; import messages from "messages"; import api from "oxalis/api/internal_api"; import { undoAction, redoAction } from "oxalis/model/actions/save_actions"; +import { setVersionRestoreModeAction } from "oxalis/model/actions/ui_actions"; import { copyAnnotationToUserAccount, finishAnnotation } from "admin/admin_rest_api"; import { location } from "libs/window"; import type { OxalisState, RestrictionsAndSettingsType, TaskType } from "oxalis/store"; @@ -52,6 +53,10 @@ class TracingActionsView extends PureComponent { Store.dispatch(undoAction()); }; + handleRestore = () => { + Store.dispatch(setVersionRestoreModeAction(true)); + }; + handleRedo = () => { Store.dispatch(redoAction()); }; @@ -232,6 +237,15 @@ class TracingActionsView extends PureComponent { ); } + if (isSkeletonMode && restrictions.allowUpdate) { + elements.push( + + + Restore older version + , + ); + } + const menu = {elements}; return ( diff --git a/app/assets/javascripts/oxalis/view/tracing_layout_view.js b/app/assets/javascripts/oxalis/view/tracing_layout_view.js index 1e635882dae..07bc70a64a5 100644 --- a/app/assets/javascripts/oxalis/view/tracing_layout_view.js +++ b/app/assets/javascripts/oxalis/view/tracing_layout_view.js @@ -7,6 +7,7 @@ import SettingsView from "oxalis/view/settings/settings_view"; import ActionBarView from "oxalis/view/action_bar_view"; import RightMenuView from "oxalis/view/right_menu_view"; import TracingView from "oxalis/view/tracing_view"; +import VersionView from "oxalis/view/version_view"; import { Layout, Icon } from "antd"; import { location } from "libs/window"; import ButtonComponent from "oxalis/view/components/button_component"; @@ -21,6 +22,7 @@ const { Header, Sider } = Layout; type StateProps = { isUpdateTracingAllowed: boolean, + isVersionRestoreActive: boolean, }; type Props = StateProps & { @@ -97,6 +99,11 @@ class TracingLayoutView extends React.PureComponent { + {this.props.isVersionRestoreActive ? ( + + + + ) : null} @@ -105,6 +112,7 @@ class TracingLayoutView extends React.PureComponent { } const mapStateToProps = (state: OxalisState): StateProps => ({ isUpdateTracingAllowed: state.tracing.restrictions.allowUpdate, + isVersionRestoreActive: state.uiInformation.isVersionRestoreActive, }); export default connect(mapStateToProps)(TracingLayoutView); diff --git a/app/assets/javascripts/oxalis/view/version_view.js b/app/assets/javascripts/oxalis/view/version_view.js new file mode 100644 index 00000000000..c00458c27e6 --- /dev/null +++ b/app/assets/javascripts/oxalis/view/version_view.js @@ -0,0 +1,172 @@ +// @flow + +import * as React from "react"; +import { Spin, Button, List, Tooltip, Icon } from "antd"; +import { ControlModeEnum } from "oxalis/constants"; +import type { OxalisState, SkeletonTracingType } from "oxalis/store"; +import { connect } from "react-redux"; +import Model from "oxalis/model"; +import { getUpdateActionLog } from "admin/admin_rest_api"; +import Store from "oxalis/store"; +import { handleGenericError } from "libs/error_handling"; +import FormattedDate from "components/formatted_date"; +import api from "oxalis/api/internal_api"; +import classNames from "classnames"; +import { setVersionRestoreModeAction } from "oxalis/model/actions/ui_actions"; +import { setAnnotationAllowUpdateAction } from "oxalis/model/actions/annotation_actions"; +import { revertToVersion } from "oxalis/model/sagas/update_actions"; +import { pushSaveQueueAction, setVersionNumberAction } from "oxalis/model/actions/save_actions"; +import { enforceSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor"; + +type Props = { + skeletonTracing: SkeletonTracingType, +}; + +type State = { + isLoading: boolean, + versions: Array>, + originalVersion: number, +}; + +class VersionView extends React.Component { + state = { + isLoading: false, + versions: [], + originalVersion: this.props.skeletonTracing.version, + }; + + componentDidMount() { + const { tracingId } = this.props.skeletonTracing; + if (tracingId != null) { + this.fetchData(tracingId); + Store.dispatch(setAnnotationAllowUpdateAction(false)); + } + } + + async fetchData(tracingId: string) { + const { url: dataStoreUrl } = Store.getState().dataset.dataStore; + this.setState({ isLoading: true }); + try { + const updateActionLog = await getUpdateActionLog(dataStoreUrl, tracingId, "skeleton"); + this.setState({ versions: updateActionLog }); + } catch (error) { + handleGenericError(error); + } finally { + this.setState({ isLoading: false }); + } + } + + async previewVersion(version: number) { + const { tracingType, annotationId } = Store.getState().tracing; + await api.tracing.restart(tracingType, annotationId, ControlModeEnum.TRACE, version); + Store.dispatch(setAnnotationAllowUpdateAction(false)); + } + + async restoreVersion(version: number) { + Store.dispatch(setVersionNumberAction(this.state.originalVersion, "skeleton")); + Store.dispatch(pushSaveQueueAction([revertToVersion(version)], "skeleton")); + await Model.save(); + Store.dispatch(setVersionRestoreModeAction(false)); + Store.dispatch(setAnnotationAllowUpdateAction(true)); + } + + handleClose = async () => { + await this.previewVersion(this.state.originalVersion); + Store.dispatch(setVersionRestoreModeAction(false)); + Store.dispatch(setAnnotationAllowUpdateAction(true)); + }; + + render() { + const VersionEntry = ({ batch, version, isNewest }) => { + const lastTimestamp = Math.max(...batch.map(entry => entry.value.actionTimestamp)); + const isActiveVersion = this.props.skeletonTracing.version === version; + const liClassName = classNames("version-entry", { + "active-version-entry": isActiveVersion, + }); + const restoreButton = ( + + ); + return ( + + + } + onClick={() => this.previewVersion(version)} + description={ + + {isNewest ? ( + + Newest version
+
+ ) : null} + {batch[0].name} and {batch.length - 1} other entries. +
+ } + /> +
+
+ ); + }; + + // TODO: server should send version numbers as part of the batches + const versionsWithVersionNumbers = this.state.versions.map((batch, index) => { + batch.forEach(action => { + action.version = this.state.versions.length - index; + }); + return batch; + }); + const filteredVersions = versionsWithVersionNumbers.filter( + (batch, index) => index === 0 || batch.length > 1 || batch[0].name !== "updateTracing", + ); + + return ( + + +

+ Version History +

+
+ ); + const { description, type } = this.getDescriptionForBatch(batch); return ( { } onClick={() => this.previewVersion(version)} + avatar={} description={ {isNewest ? ( @@ -109,7 +163,7 @@ class VersionView extends React.Component { Newest version
) : null} - {batch[0].name} and {batch.length - 1} other entries. + {description}
} /> @@ -121,7 +175,7 @@ class VersionView extends React.Component { // TODO: server should send version numbers as part of the batches const versionsWithVersionNumbers = this.state.versions.map((batch, index) => { batch.forEach(action => { - action.version = this.state.versions.length - index; + action.value.version = this.state.versions.length - index; }); return batch; }); @@ -151,9 +205,9 @@ class VersionView extends React.Component { {filteredVersions.map((batch, index) => ( ))} From bc317a4abbcc367da676c032488aa818cc2a3cfe Mon Sep 17 00:00:00 2001 From: Daniel Werner Date: Tue, 11 Sep 2018 10:21:42 +0200 Subject: [PATCH 03/13] improve batch description, save before opening version restore view --- .../view/action-bar/tracing_actions_view.js | 3 +- .../javascripts/oxalis/view/version_view.js | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) 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 d605e175681..40dd2b13c89 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 @@ -53,7 +53,8 @@ class TracingActionsView extends PureComponent { Store.dispatch(undoAction()); }; - handleRestore = () => { + handleRestore = async () => { + await Model.save(); Store.dispatch(setVersionRestoreModeAction(true)); }; diff --git a/app/assets/javascripts/oxalis/view/version_view.js b/app/assets/javascripts/oxalis/view/version_view.js index 3b4c1afdde7..35eca262866 100644 --- a/app/assets/javascripts/oxalis/view/version_view.js +++ b/app/assets/javascripts/oxalis/view/version_view.js @@ -1,6 +1,7 @@ // @flow import * as React from "react"; +import _ from "lodash"; import { Spin, Button, List, Tooltip, Icon, Avatar } from "antd"; import { ControlModeEnum } from "oxalis/constants"; import { connect } from "react-redux"; @@ -46,8 +47,8 @@ class VersionView extends React.Component { }; componentDidMount() { - this.fetchData(this.props.skeletonTracing.tracingId); Store.dispatch(setAnnotationAllowUpdateAction(false)); + this.fetchData(this.props.skeletonTracing.tracingId); } async fetchData(tracingId: string) { @@ -64,41 +65,45 @@ class VersionView extends React.Component { } getDescriptionForBatch(batch: Array): { description: string, type: string } { - const moveTreeComponentUA = batch.find(ua => ua.name === "moveTreeComponent"); - if (moveTreeComponentUA != null) { - if (batch.some(ua => ua.name === "createTree")) { + const groupedUpdateActions = _.groupBy(batch, "name"); + + const moveTreeComponentUAs = groupedUpdateActions.moveTreeComponent; + if (moveTreeComponentUAs != null) { + if (groupedUpdateActions.createTree != null) { return { - description: `Split off a tree with ${moveTreeComponentUA.value.nodeIds.length} nodes.`, + description: `Split off a tree with ${ + moveTreeComponentUAs[0].value.nodeIds.length + } nodes.`, type: "arrows-alt", }; - } else if (batch.some(ua => ua.name === "deleteTree")) { + } else if (groupedUpdateActions.deleteTree != null) { return { - description: `Merged a tree with ${moveTreeComponentUA.value.nodeIds.length} nodes.`, + description: `Merged a tree with ${moveTreeComponentUAs[0].value.nodeIds.length} nodes.`, type: "shrink", }; } } - const deleteTreeUA = batch.find(ua => ua.name === "deleteTree"); - if (deleteTreeUA != null) { + const deleteTreeUAs = groupedUpdateActions.deleteTree; + if (deleteTreeUAs != null) { return { - description: `Deleted tree with id ${deleteTreeUA.value.id}.`, + description: `Deleted the tree with id ${deleteTreeUAs[0].value.id}.`, type: "delete", }; } - const deleteNodeUA = batch.find(ua => ua.name === "deleteNode"); - if (deleteNodeUA != null) { + const deleteNodeUAs = groupedUpdateActions.deleteNode; + if (deleteNodeUAs != null) { return { - description: `Deleted node with id ${deleteNodeUA.value.nodeId}.`, + description: `Deleted the node with id ${deleteNodeUAs[0].value.nodeId}.`, type: "delete", }; } - const revertToVersionUA = batch.find(ua => ua.name === "revertToVersion"); - if (revertToVersionUA != null) { + const revertToVersionUAs = groupedUpdateActions.revertToVersion; + if (revertToVersionUAs != null) { return { - description: `Reverted to version ${revertToVersionUA.value.sourceVersion}.`, + description: `Reverted to version ${revertToVersionUAs[0].value.sourceVersion}.`, type: "backward", }; } From f8a54f15db50f3269ef45e41ffc811592afd1822 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 11 Sep 2018 14:23:11 +0200 Subject: [PATCH 04/13] rework flow types for ServerUpdateActions --- .../oxalis/model/sagas/update_actions.js | 140 +++++++++++------- .../javascripts/oxalis/view/version_view.js | 37 +++-- 2 files changed, 105 insertions(+), 72 deletions(-) diff --git a/app/assets/javascripts/oxalis/model/sagas/update_actions.js b/app/assets/javascripts/oxalis/model/sagas/update_actions.js index f33ce7440ee..b70a3ce5108 100644 --- a/app/assets/javascripts/oxalis/model/sagas/update_actions.js +++ b/app/assets/javascripts/oxalis/model/sagas/update_actions.js @@ -14,9 +14,9 @@ import { convertFrontendBoundingBoxToServer } from "oxalis/model/reducers/reduce export type NodeWithTreeIdType = { treeId: number } & NodeType; -type UpdateTreeUpdateAction = { +type UpdateTreeUpdateAction = {| name: "createTree" | "updateTree", - value: { + value: {| id: number, updatedId: ?number, color: Vector3, @@ -24,106 +24,104 @@ type UpdateTreeUpdateAction = { comments: Array, branchPoints: Array, groupId: ?number, - }, -}; -type DeleteTreeUpdateAction = { + timestamp: number, + |}, +|}; +type DeleteTreeUpdateAction = {| name: "deleteTree", - value: { id: number }, -}; -type MoveTreeComponentUpdateAction = { + value: {| id: number |}, +|}; +type MoveTreeComponentUpdateAction = {| name: "moveTreeComponent", - value: { + value: {| sourceId: number, targetId: number, nodeIds: Array, - }, -}; -type MergeTreeUpdateAction = { + |}, +|}; +type MergeTreeUpdateAction = {| name: "mergeTree", - value: { + value: {| sourceId: number, targetId: number, - }, -}; -type CreateNodeUpdateAction = { + |}, +|}; +type CreateNodeUpdateAction = {| name: "createNode", value: NodeWithTreeIdType, -}; -type UpdateNodeUpdateAction = { +|}; +type UpdateNodeUpdateAction = {| name: "updateNode", value: NodeWithTreeIdType, -}; -type ToggleTreeUpdateAction = { +|}; +type ToggleTreeUpdateAction = {| name: "toggleTree", - value: { + value: {| id: number, - }, -}; -type DeleteNodeUpdateAction = { + |}, +|}; +type DeleteNodeUpdateAction = {| name: "deleteNode", - value: { + value: {| treeId: number, nodeId: number, - }, -}; -type CreateEdgeUpdateAction = { + |}, +|}; +type CreateEdgeUpdateAction = {| name: "createEdge", - value: { + value: {| treeId: number, source: number, target: number, - }, -}; -type DeleteEdgeUpdateAction = { + |}, +|}; +type DeleteEdgeUpdateAction = {| name: "deleteEdge", - value: { + value: {| treeId: number, source: number, target: number, - }, -}; -type UpdateSkeletonTracingUpdateAction = { + |}, +|}; +type UpdateSkeletonTracingUpdateAction = {| name: "updateTracing", - value: { + value: {| activeNode: ?number, editPosition: Vector3, editRotation: Vector3, userBoundingBox: ?BoundingBoxObjectType, zoomLevel: number, - }, -}; -type UpdateVolumeTracingUpdateAction = { + |}, +|}; +type UpdateVolumeTracingUpdateAction = {| name: "updateTracing", - value: { + value: {| activeSegmentId: number, editPosition: Vector3, editRotation: Vector3, largestSegmentId: number, userBoundingBox: ?BoundingBoxObjectType, zoomLevel: number, - }, -}; -type UpdateBucketUpdateAction = { + |}, +|}; +type UpdateBucketUpdateAction = {| name: "updateBucket", value: SendBucketInfo & { base64Data: string, }, -}; -type UpdateTreeGroupsUpdateAction = { +|}; +type UpdateTreeGroupsUpdateAction = {| name: "updateTreeGroups", - value: { + value: {| treeGroups: Array, - }, -}; -type RevertToVersionUpdateAction = { + |}, +|}; +type RevertToVersionUpdateAction = {| name: "revertToVersion", - value: { + value: {| sourceVersion: number, - }, -}; -type UpdateTracingUpdateAction = - | UpdateSkeletonTracingUpdateAction - | UpdateVolumeTracingUpdateAction; + |}, +|}; export type UpdateAction = | UpdateTreeUpdateAction @@ -135,12 +133,40 @@ export type UpdateAction = | DeleteNodeUpdateAction | CreateEdgeUpdateAction | DeleteEdgeUpdateAction - | UpdateTracingUpdateAction + | UpdateSkeletonTracingUpdateAction + | UpdateVolumeTracingUpdateAction | UpdateBucketUpdateAction | ToggleTreeUpdateAction | RevertToVersionUpdateAction | UpdateTreeGroupsUpdateAction; +type AddServerValuesFn = ( + T, +) => { + ...T, + value: { ...$PropertyType, actionTimestamp: number, version: number }, +}; +type AsServerAction = $Call; + +// Since flow does not provide ways to perform type transformations on the +// single parts of a union, we need to write this out manually. +export type ServerUpdateAction = + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction + | AsServerAction; + export function createTree(tree: TreeType): UpdateTreeUpdateAction { return { name: "createTree", diff --git a/app/assets/javascripts/oxalis/view/version_view.js b/app/assets/javascripts/oxalis/view/version_view.js index 35eca262866..9a0effe48ef 100644 --- a/app/assets/javascripts/oxalis/view/version_view.js +++ b/app/assets/javascripts/oxalis/view/version_view.js @@ -18,16 +18,7 @@ import { revertToVersion } from "oxalis/model/sagas/update_actions"; import { pushSaveQueueAction, setVersionNumberAction } from "oxalis/model/actions/save_actions"; import { enforceSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor"; import type { OxalisState, SkeletonTracingType } from "oxalis/store"; -import type { UpdateAction } from "oxalis/model/sagas/update_actions"; - -type AddToValueFn = ( - T, -) => { - name: $PropertyType, - value: { ...$PropertyType, actionTimestamp: number, version: number }, -}; - -type ServerUpdateAction = $Call; +import type { ServerUpdateAction } from "oxalis/model/sagas/update_actions"; type Props = { skeletonTracing: SkeletonTracingType, @@ -69,41 +60,57 @@ class VersionView extends React.Component { const moveTreeComponentUAs = groupedUpdateActions.moveTreeComponent; if (moveTreeComponentUAs != null) { + const firstMoveTreeComponentUA = moveTreeComponentUAs[0]; + if (firstMoveTreeComponentUA.name !== "moveTreeComponent") { + throw new Error("Flow constraint violated"); + } if (groupedUpdateActions.createTree != null) { return { description: `Split off a tree with ${ - moveTreeComponentUAs[0].value.nodeIds.length + firstMoveTreeComponentUA.value.nodeIds.length } nodes.`, type: "arrows-alt", }; } else if (groupedUpdateActions.deleteTree != null) { return { - description: `Merged a tree with ${moveTreeComponentUAs[0].value.nodeIds.length} nodes.`, + description: `Merged a tree with ${firstMoveTreeComponentUA.value.nodeIds.length} nodes.`, type: "shrink", }; } } const deleteTreeUAs = groupedUpdateActions.deleteTree; + const firstDeleteTreeUA = deleteTreeUAs[0]; + if (firstDeleteTreeUA.name !== "deleteTree") { + throw new Error("Flow constraint violated"); + } if (deleteTreeUAs != null) { return { - description: `Deleted the tree with id ${deleteTreeUAs[0].value.id}.`, + description: `Deleted the tree with id ${firstDeleteTreeUA.value.id}.`, type: "delete", }; } const deleteNodeUAs = groupedUpdateActions.deleteNode; + const firstDeleteNodeUA = deleteNodeUAs[0]; + if (firstDeleteNodeUA.name !== "deleteNode") { + throw new Error("Flow constraint violated"); + } if (deleteNodeUAs != null) { return { - description: `Deleted the node with id ${deleteNodeUAs[0].value.nodeId}.`, + description: `Deleted the node with id ${firstDeleteNodeUA.value.nodeId}.`, type: "delete", }; } const revertToVersionUAs = groupedUpdateActions.revertToVersion; + const firstRevertToVersionUA = revertToVersionUAs[0]; + if (firstRevertToVersionUA.name !== "revertToVersion") { + throw new Error("Flow constraint violated"); + } if (revertToVersionUAs != null) { return { - description: `Reverted to version ${revertToVersionUAs[0].value.sourceVersion}.`, + description: `Reverted to version ${firstRevertToVersionUA.value.sourceVersion}.`, type: "backward", }; } From a5fbd4f84205a53f9c38b2803913d35d3ee1c251 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 11 Sep 2018 14:40:56 +0200 Subject: [PATCH 05/13] fix missing null checks --- .../javascripts/oxalis/view/version_view.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/oxalis/view/version_view.js b/app/assets/javascripts/oxalis/view/version_view.js index 9a0effe48ef..02dc6652e56 100644 --- a/app/assets/javascripts/oxalis/view/version_view.js +++ b/app/assets/javascripts/oxalis/view/version_view.js @@ -80,11 +80,11 @@ class VersionView extends React.Component { } const deleteTreeUAs = groupedUpdateActions.deleteTree; - const firstDeleteTreeUA = deleteTreeUAs[0]; - if (firstDeleteTreeUA.name !== "deleteTree") { + if (deleteTreeUAs != null) { + const firstDeleteTreeUA = deleteTreeUAs[0]; + if (firstDeleteTreeUA.name !== "deleteTree") { throw new Error("Flow constraint violated"); } - if (deleteTreeUAs != null) { return { description: `Deleted the tree with id ${firstDeleteTreeUA.value.id}.`, type: "delete", @@ -92,11 +92,11 @@ class VersionView extends React.Component { } const deleteNodeUAs = groupedUpdateActions.deleteNode; - const firstDeleteNodeUA = deleteNodeUAs[0]; - if (firstDeleteNodeUA.name !== "deleteNode") { - throw new Error("Flow constraint violated"); - } if (deleteNodeUAs != null) { + const firstDeleteNodeUA = deleteNodeUAs[0]; + if (firstDeleteNodeUA.name !== "deleteNode") { + throw new Error("Flow constraint violated"); + } return { description: `Deleted the node with id ${firstDeleteNodeUA.value.nodeId}.`, type: "delete", @@ -104,11 +104,11 @@ class VersionView extends React.Component { } const revertToVersionUAs = groupedUpdateActions.revertToVersion; - const firstRevertToVersionUA = revertToVersionUAs[0]; - if (firstRevertToVersionUA.name !== "revertToVersion") { - throw new Error("Flow constraint violated"); - } if (revertToVersionUAs != null) { + const firstRevertToVersionUA = revertToVersionUAs[0]; + if (firstRevertToVersionUA.name !== "revertToVersion") { + throw new Error("Flow constraint violated"); + } return { description: `Reverted to version ${firstRevertToVersionUA.value.sourceVersion}.`, type: "backward", From 450572970e987d17c2db01b29c8e470563db0e62 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Tue, 11 Sep 2018 16:07:25 +0200 Subject: [PATCH 06/13] only scroll version list in version history view --- .../javascripts/oxalis/view/version_view.js | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/oxalis/view/version_view.js b/app/assets/javascripts/oxalis/view/version_view.js index 02dc6652e56..a7c6dae323e 100644 --- a/app/assets/javascripts/oxalis/view/version_view.js +++ b/app/assets/javascripts/oxalis/view/version_view.js @@ -116,7 +116,7 @@ class VersionView extends React.Component { } return { description: `${batch[0].name} and ${batch.length - 1} other entries.`, - type: "plus-circle", + type: "plus", }; } @@ -167,7 +167,7 @@ class VersionView extends React.Component { } onClick={() => this.previewVersion(version)} - avatar={} + avatar={} description={ {isNewest ? ( @@ -196,35 +196,37 @@ class VersionView extends React.Component { ); return ( - - -

- Version History -

-
-
); - const { description, type } = this.getDescriptionForBatch(batch); + const { description, type } = this.getDescriptionForBatch(actions); return ( { actions={isActiveVersion && !isNewest ? [restoreButton] : []} > } + title={ + + Version {version} () + + } onClick={() => this.previewVersion(version)} avatar={} description={ @@ -184,15 +242,9 @@ class VersionView extends React.Component { ); }; - // TODO: server should send version numbers as part of the batches - const versionsWithVersionNumbers = this.state.versions.map((batch, index) => { - batch.forEach(action => { - action.value.version = this.state.versions.length - index; - }); - return batch; - }); - const filteredVersions = versionsWithVersionNumbers.filter( - (batch, index) => index === 0 || batch.length > 1 || batch[0].name !== "updateTracing", + const filteredVersions = this.state.versions.filter( + (batch, index) => + index === 0 || batch.value.length > 1 || batch.value[0].name !== "updateTracing", ); return ( @@ -221,16 +273,18 @@ class VersionView extends React.Component {
- - {filteredVersions.map((batch, index) => ( - - ))} - + + + {filteredVersions.map((batch, index) => ( + + ))} + +
diff --git a/app/assets/stylesheets/trace_view/_tracing_view.less b/app/assets/stylesheets/trace_view/_tracing_view.less index b7ad70effb0..34d75237bde 100644 --- a/app/assets/stylesheets/trace_view/_tracing_view.less +++ b/app/assets/stylesheets/trace_view/_tracing_view.less @@ -126,7 +126,7 @@ .version-entry { padding: 3px; - &:hover { + &:hover:not(.active-version-entry) { background-color: #e6f7ff; } } diff --git a/package.json b/package.json index 2f87c5a53f8..eacac8bbc16 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "react-virtualized": "^9.20.1", "redux": "^3.6.0", "redux-saga": "^0.16.0", - "scroll-into-view-if-needed": "2.2.8", + "react-scroll-into-view-if-needed": "2.1.6", "stats.js": "^1.0.0", "three": "^0.87.0", "tween.js": "^16.3.1", diff --git a/yarn.lock b/yarn.lock index 771531b60df..f4d0e50522d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8738,6 +8738,10 @@ react-router@^4.2.0: prop-types "^15.5.4" warning "^3.0.0" +react-scroll-into-view-if-needed@2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/react-scroll-into-view-if-needed/-/react-scroll-into-view-if-needed-2.1.6.tgz#e4ac5614b942157ef9bbff9205b13a8a8effacea" + react-slick@~0.23.1: version "0.23.1" resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.23.1.tgz#15791c4107f0ba3a5688d5bd97b7b7ceaa0dd181" @@ -9558,10 +9562,6 @@ scoped-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" -scroll-into-view-if-needed@2.2.8: - version "2.2.8" - resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.8.tgz#c22ffbddce5c8a31949ab3e01c27a6c29ba7b979" - semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" From f39f289ff53f8a993024f4d411f7287079bebce7 Mon Sep 17 00:00:00 2001 From: Daniel Werner Date: Wed, 12 Sep 2018 15:47:58 +0200 Subject: [PATCH 11/13] adapt changelog and docs --- CHANGELOG.md | 2 ++ .../javascripts/oxalis/view/action-bar/tracing_actions_view.js | 2 +- docs/tracing_ui.md | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dedffbec41f..ee17bb1d792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). - Improved security by enabling http security headers. [#3084](https://github.com/scalableminds/webknossos/pull/3084) - Added the possibility to write markdown in the annotation description. [#3081](https://github.com/scalableminds/webknossos/pull/3081) +- Added a view to restore any older version of a skeleton tracing. Access it through the dropdown next to the Save button. [#3194](https://github.com/scalableminds/webknossos/pull/3194) +![version-restore-highlight](https://user-images.githubusercontent.com/1702075/45428378-6842d380-b6a1-11e8-88c2-e4ffcd762cd5.png) - Added the brush size to the settings on the left in volume tracing. The size can now also be adjusted by using only the keyboard. [#3126](https://github.com/scalableminds/webknossos/pull/3126) - Added a user documentation for webKnossos [#3011](https://github.com/scalableminds/webknossos/pull/3011) - Tree groups can now be activated. This allows to rename a tree group analogous to renaming a tree. Also, toggling the visibility of a tree group can now be done by using the shortcuts "1" and "2". [#3066](https://github.com/scalableminds/webknossos/pull/3066) 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 b4f511a8610..3dbd221f288 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 @@ -242,7 +242,7 @@ class TracingActionsView extends PureComponent { elements.push( - Restore older version + Restore Older Version , ); } diff --git a/docs/tracing_ui.md b/docs/tracing_ui.md index a640d2ae3bf..181417a29f9 100644 --- a/docs/tracing_ui.md +++ b/docs/tracing_ui.md @@ -20,6 +20,7 @@ The most common buttons are: - `Download`: Starts the download of the current annotation. Skeleton annotations are downloaded as [NML](./data_formats.md#nml) files. Volume annotation downloads contain the raw segmentation data as [WKW](./data_formats.md#wkw) files. - `Share`: Create a shareable link to your dataset containing the current position, rotation, zoom level etc. Use this to collaboratively work with colleagues. Read more about this feature in the [Sharing guide](./sharing.md). - `Add Script`: Using the [webKnossos frontend API](https://demo.webknossos.org/assets/docs/frontend-api/index.html) users can interact with webKnossos programmatically. User scripts can be executed from here. Admins can add often used scripts to webKnossos to make them available to all users for easy access. +- `Restore Older Version`: Only available for skeleton tracings. Opens a view that shows all previous versions of a skeleton tracing. From this view any older version can be selected, previewed, and restored. A user can directly jump to positions within their datasets by entering them in the position input field. The same is true for the rotation in some tracing modes. From 58e57b1cb02a7cb7425ed31c28ff438c075bb74f Mon Sep 17 00:00:00 2001 From: Daniel Werner Date: Wed, 12 Sep 2018 15:55:05 +0200 Subject: [PATCH 12/13] [docs] point to 'restore older version' from redo/undo, fix typo --- docs/tracing_ui.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tracing_ui.md b/docs/tracing_ui.md index 181417a29f9..40bf0043349 100644 --- a/docs/tracing_ui.md +++ b/docs/tracing_ui.md @@ -14,13 +14,13 @@ The toolbar contains frequently used commands, your current position within the The most common buttons are: - `Settings`: Toggles the visibility of the setting menu on the left-hand side to provide more space for your data. -- `Undo` / `Redo`: Undoes the last operation or redoes it if now changes have been made in the meantime. +- `Undo` / `Redo`: Undoes the last operation or redoes it if no new changes have been made in the meantime. Undo can only revert changes made in this session (since the moment the tracing view was opened). To revert to older versions use the "Restore Older Version" functionality, described later in this list. - `Save`: Saves your annotation work. webKnossos automatically saves every 30 seconds. - `Archive`: Only available for Explorative Annotations. Closes the annotation and archives it, removing it from a user's dashboard. Archived annotations can be found on a user's dashboard under "Explorative Annotations" and by clicking on "Show Archived Annotations". Use this to declutter your dashboard. - `Download`: Starts the download of the current annotation. Skeleton annotations are downloaded as [NML](./data_formats.md#nml) files. Volume annotation downloads contain the raw segmentation data as [WKW](./data_formats.md#wkw) files. - `Share`: Create a shareable link to your dataset containing the current position, rotation, zoom level etc. Use this to collaboratively work with colleagues. Read more about this feature in the [Sharing guide](./sharing.md). - `Add Script`: Using the [webKnossos frontend API](https://demo.webknossos.org/assets/docs/frontend-api/index.html) users can interact with webKnossos programmatically. User scripts can be executed from here. Admins can add often used scripts to webKnossos to make them available to all users for easy access. -- `Restore Older Version`: Only available for skeleton tracings. Opens a view that shows all previous versions of a skeleton tracing. From this view any older version can be selected, previewed, and restored. +- `Restore Older Version`: Only available for skeleton tracings. Opens a view that shows all previous versions of a skeleton tracing. From this view, any older version can be selected, previewed, and restored. A user can directly jump to positions within their datasets by entering them in the position input field. The same is true for the rotation in some tracing modes. From 3e9c150a3d40fe9bbdf35777b6292e0a0a25c6fb Mon Sep 17 00:00:00 2001 From: Daniel Werner Date: Mon, 17 Sep 2018 15:45:37 +0200 Subject: [PATCH 13/13] make version restore view's position fixed, style fixes, allow showVersionRestore url parameter that opens the version restore view without fetching the newest tracing --- app/assets/javascripts/oxalis/controller.js | 4 +++ .../oxalis/controller/url_manager.js | 2 +- .../model/sagas/skeletontracing_saga.js | 10 ++++++ .../view/layouting/tracing_layout_view.js | 2 +- .../javascripts/oxalis/view/version_view.js | 35 +++++++++---------- .../stylesheets/trace_view/_tracing_view.less | 13 +++++++ package.json | 1 - yarn.lock | 4 --- 8 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/oxalis/controller.js b/app/assets/javascripts/oxalis/controller.js index 6aa6f936ee6..86517d8e8f9 100644 --- a/app/assets/javascripts/oxalis/controller.js +++ b/app/assets/javascripts/oxalis/controller.js @@ -83,11 +83,15 @@ class Controller extends React.PureComponent { Toast.error(messages["webgl.disabled"]); } + // Preview a working tracing version if the showVersionRestore URL parameter is supplied + const version = Utils.hasUrlParam("showVersionRestore") ? 1 : undefined; + Model.fetch( this.props.initialTracingType, this.props.initialAnnotationId, this.props.initialControlmode, true, + version, ) .then(() => this.modelFetchDone()) .catch(error => { diff --git a/app/assets/javascripts/oxalis/controller/url_manager.js b/app/assets/javascripts/oxalis/controller/url_manager.js index 30bbed732d6..142f32e369d 100644 --- a/app/assets/javascripts/oxalis/controller/url_manager.js +++ b/app/assets/javascripts/oxalis/controller/url_manager.js @@ -133,7 +133,7 @@ export function updateTypeAndId( // will only ever be updated for the annotations route as the other route is for // dataset viewing only return baseUrl.replace( - /^(.*\/annotations)\/(.*?)\/([^/]*)(\/?.*)$/, + /^(.*\/annotations)\/(.*?)\/([^/?]*)(\/?.*)$/, (all, base, type, id, rest) => `${base}/${tracingType}/${annotationId}${rest}`, ); } diff --git a/app/assets/javascripts/oxalis/model/sagas/skeletontracing_saga.js b/app/assets/javascripts/oxalis/model/sagas/skeletontracing_saga.js index eac92620b1b..b690de2369e 100644 --- a/app/assets/javascripts/oxalis/model/sagas/skeletontracing_saga.js +++ b/app/assets/javascripts/oxalis/model/sagas/skeletontracing_saga.js @@ -16,6 +16,7 @@ import { _takeEvery, select, race, + call, } from "oxalis/model/sagas/effect-generators"; import type { Saga } from "oxalis/model/sagas/effect-generators"; import { @@ -58,6 +59,7 @@ import type { UpdateAction } from "oxalis/model/sagas/update_actions"; import api from "oxalis/api/internal_api"; import DiffableMap, { diffDiffableMaps } from "libs/diffable_map"; import EdgeCollection, { diffEdgeCollections } from "oxalis/model/edge_collection"; +import { setVersionRestoreVisibilityAction } from "oxalis/model/actions/ui_actions"; function* centerActiveNode(action: ActionType): Saga { getActiveNode(yield* select((state: OxalisState) => enforceSkeletonTracing(state.tracing))).map( @@ -131,6 +133,13 @@ export function* watchTreeNames(): Saga { } } +export function* watchVersionRestoreParam(): Saga { + const showVersionRestore = yield* call(Utils.hasUrlParam, "showVersionRestore"); + if (showVersionRestore) { + yield* put(setVersionRestoreVisibilityAction(true)); + } +} + export function* watchSkeletonTracingAsync(): Saga { yield* take("INITIALIZE_SKELETONTRACING"); yield _takeEvery("WK_READY", watchTreeNames); @@ -149,6 +158,7 @@ export function* watchSkeletonTracingAsync(): Saga { ); yield* fork(watchFailedNodeCreations); yield* fork(watchBranchPointDeletion); + yield* fork(watchVersionRestoreParam); } function* diffNodes( 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 99eaddf6d6d..cbf71687204 100644 --- a/app/assets/javascripts/oxalis/view/layouting/tracing_layout_view.js +++ b/app/assets/javascripts/oxalis/view/layouting/tracing_layout_view.js @@ -181,7 +181,7 @@ class TracingLayoutView extends React.PureComponent { {this.props.showVersionRestore ? ( - + ) : null} diff --git a/app/assets/javascripts/oxalis/view/version_view.js b/app/assets/javascripts/oxalis/view/version_view.js index 7617f62ee90..fa501f0249d 100644 --- a/app/assets/javascripts/oxalis/view/version_view.js +++ b/app/assets/javascripts/oxalis/view/version_view.js @@ -17,7 +17,6 @@ import { setAnnotationAllowUpdateAction } from "oxalis/model/actions/annotation_ import { revertToVersion } from "oxalis/model/sagas/update_actions"; import { pushSaveQueueAction, setVersionNumberAction } from "oxalis/model/actions/save_actions"; import { enforceSkeletonTracing } from "oxalis/model/accessors/skeletontracing_accessor"; -import ScrollIntoViewIfNeeded from "react-scroll-into-view-if-needed"; import type { OxalisState, SkeletonTracingType } from "oxalis/store"; import type { ServerUpdateAction, @@ -38,7 +37,6 @@ type Props = { type State = { isLoading: boolean, versions: Array, - originalVersion: number, }; const descriptionFns = { @@ -76,7 +74,6 @@ class VersionView extends React.Component { state = { isLoading: false, versions: [], - originalVersion: this.props.skeletonTracing.version, }; componentDidMount() { @@ -97,6 +94,10 @@ class VersionView extends React.Component { } } + getNewestVersion(): number { + return _.max(this.state.versions.map(batch => batch.version)) || 0; + } + getDescriptionForSpecificBatch( actions: Array, type: string, @@ -181,7 +182,7 @@ class VersionView extends React.Component { } async restoreVersion(version: number) { - Store.dispatch(setVersionNumberAction(this.state.originalVersion, "skeleton")); + Store.dispatch(setVersionNumberAction(this.getNewestVersion(), "skeleton")); Store.dispatch(pushSaveQueueAction([revertToVersion(version)], "skeleton")); await Model.save(); Store.dispatch(setVersionRestoreVisibilityAction(false)); @@ -189,7 +190,7 @@ class VersionView extends React.Component { } handleClose = async () => { - await this.previewVersion(this.state.originalVersion); + await this.previewVersion(this.getNewestVersion()); Store.dispatch(setVersionRestoreVisibilityAction(false)); Store.dispatch(setAnnotationAllowUpdateAction(true)); }; @@ -249,7 +250,7 @@ class VersionView extends React.Component { return (
-
+

Version History

- - - {filteredVersions.map((batch, index) => ( - - ))} - - + + {filteredVersions.map((batch, index) => ( + + ))} +
diff --git a/app/assets/stylesheets/trace_view/_tracing_view.less b/app/assets/stylesheets/trace_view/_tracing_view.less index b1a17818172..5770cfd497a 100644 --- a/app/assets/stylesheets/trace_view/_tracing_view.less +++ b/app/assets/stylesheets/trace_view/_tracing_view.less @@ -1,3 +1,5 @@ +@import "../_variables.less"; + #tracing { position: relative; display: inline-block; @@ -124,6 +126,17 @@ min-width: 450px; } +#version-restore-sider { + border-left: 1px solid #e8e8e8; + padding: 3; + padding-top: 15px; + position: fixed; + right: 0; + z-index: 1000; + top: @navbar-height; + bottom: 0; +} + .version-entry { padding: 3px; &:hover:not(.active-version-entry) { diff --git a/package.json b/package.json index 1c16fdb35d9..9226cd0d946 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,6 @@ "react-virtualized": "^9.20.1", "redux": "^3.6.0", "redux-saga": "^0.16.0", - "react-scroll-into-view-if-needed": "2.1.6", "stats.js": "^1.0.0", "three": "^0.87.0", "tween.js": "^16.3.1", diff --git a/yarn.lock b/yarn.lock index f7a5dd2cc9e..c76440fd882 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8777,10 +8777,6 @@ react-router@^4.2.0: prop-types "^15.5.4" warning "^3.0.0" -react-scroll-into-view-if-needed@2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/react-scroll-into-view-if-needed/-/react-scroll-into-view-if-needed-2.1.6.tgz#e4ac5614b942157ef9bbff9205b13a8a8effacea" - react-slick@~0.23.1: version "0.23.1" resolved "https://registry.yarnpkg.com/react-slick/-/react-slick-0.23.1.tgz#15791c4107f0ba3a5688d5bd97b7b7ceaa0dd181"