diff --git a/js/components/datasets/crossReferenceDialog.js b/js/components/datasets/crossReferenceDialog.js index d8aeb4a4a..ccb7f5f47 100644 --- a/js/components/datasets/crossReferenceDialog.js +++ b/js/components/datasets/crossReferenceDialog.js @@ -232,141 +232,160 @@ export const CrossReferenceDialog = memo( moleculeList ); + if (anchorEl === null) { + dispatch(resetCrossReferenceDialog()); + } + return ( - - - dispatch(resetCrossReferenceDialog())} + <> + {anchorEl && anchorEl !== null && ( + <> + + + dispatch(resetCrossReferenceDialog())} + > + + + + ]} > - - - - ]} - > - {isLoadingCrossReferenceScores === false && moleculeList && ( - <> - - - {moleculeList.length > 0 && ( - - - - - - - - + {isLoadingCrossReferenceScores === false && moleculeList && ( + <> + + + {moleculeList.length > 0 && ( - + + + + + + + + + + + + + {/* C stands for contacts now */} + + + + - - - - {/* C stands for contacts now */} - - - + )} - )} - - -
- {moleculeList.length > 0 && - moleculeList.map((data, index, array) => { - let molecule = Object.assign({ isCrossReference: true }, data.molecule); - let previousData = index > 0 && Object.assign({ isCrossReference: true }, array[index - 1]); - let nextData = index < array?.length && Object.assign({ isCrossReference: true }, array[index + 1]); +
+ {moleculeList.length > 0 && + moleculeList.map((data, index, array) => { + let molecule = Object.assign({ isCrossReference: true }, data.molecule); + let previousData = index > 0 && Object.assign({ isCrossReference: true }, array[index - 1]); + let nextData = + index < array?.length && Object.assign({ isCrossReference: true }, array[index + 1]); - return ( - - ); - })} - {!(moleculeList.length > 0) && ( - + return ( + + ); + })} + {!(moleculeList.length > 0) && ( + + + No molecules found! + + + )} +
+ + )} + {isLoadingCrossReferenceScores === true && ( + - No molecules found! + )} -
- - )} - {isLoadingCrossReferenceScores === true && ( - - - - - - )} -
-
+ + + + )} + ); }) ); diff --git a/js/components/header/index.js b/js/components/header/index.js index db831a1f0..00903d4fa 100644 --- a/js/components/header/index.js +++ b/js/components/header/index.js @@ -227,9 +227,6 @@ export default memo( - - - dispatch => { +export const removeLigand = (stage, data, skipTracking = false, withVector = true) => dispatch => { dispatch(deleteObject(Object.assign({ display_div: VIEWS.MAJOR_VIEW }, generateMoleculeObject(data)), stage)); dispatch(removeFromFragmentDisplayList(generateMoleculeId(data), skipTracking)); - // remove vector - dispatch(removeVector(stage, data, skipTracking)); + if (withVector === true) { + // remove vector + dispatch(removeVector(stage, data, skipTracking)); + } }; /** diff --git a/js/components/preview/viewerControls/index.js b/js/components/preview/viewerControls/index.js index 5d85a5ef4..e174aa379 100644 --- a/js/components/preview/viewerControls/index.js +++ b/js/components/preview/viewerControls/index.js @@ -10,7 +10,7 @@ import { ButtonGroup, Grid, makeStyles, Tooltip } from '@material-ui/core'; import { SettingControls } from './settingsControls'; import DisplayControls from './displayControls/'; import { MouseControls } from './mouseControls'; -import { ActionCreators as UndoActionCreators } from 'redux-undo'; +import { ActionCreators as UndoActionCreators } from '../../../undoredo/actions'; import { undoAction, redoAction, @@ -226,7 +226,7 @@ export const ViewerControls = memo(({}) => { startIcon={} className={classes.buttonMargin} > - Restore view + Restore clip/slab/centre diff --git a/js/components/preview/viewerControls/settingsControls.js b/js/components/preview/viewerControls/settingsControls.js index 46301a824..53f415515 100644 --- a/js/components/preview/viewerControls/settingsControls.js +++ b/js/components/preview/viewerControls/settingsControls.js @@ -8,6 +8,7 @@ import { setNglBckGrndColor, setNglClipNear, setNglClipFar, setNglClipDist, setN import { NglContext } from '../../nglView/nglProvider'; import { VIEWS } from '../../../constants/constants'; + const useStyles = makeStyles(theme => ({ root: { width: '100%', @@ -44,6 +45,8 @@ export const SettingControls = memo(({ open, onClose }) => { } }; + + return ( diff --git a/js/components/snapshot/redux/dispatchActions.js b/js/components/snapshot/redux/dispatchActions.js index b20e7631f..3fa268fdf 100644 --- a/js/components/snapshot/redux/dispatchActions.js +++ b/js/components/snapshot/redux/dispatchActions.js @@ -10,6 +10,7 @@ import { setSharedSnapshot, setSnapshotJustSaved } from './actions'; +import { setDialogCurrentStep } from '../../snapshot/redux/actions'; import { DJANGO_CONTEXT } from '../../../utils/djangoContext'; import { assignSnapshotToProject, @@ -306,6 +307,7 @@ export const createNewSnapshot = ({ dispatch(setOpenSnapshotSavingDialog(false)); dispatch(setIsLoadingSnapshotDialog(false)); dispatch(setSnapshotJustSaved(projectResponse.data.id)); + dispatch(setDialogCurrentStep()); } }) .catch(error => { diff --git a/js/components/tracking/trackingModal.js b/js/components/tracking/trackingModal.js index 09cb10118..aaa42b69a 100644 --- a/js/components/tracking/trackingModal.js +++ b/js/components/tracking/trackingModal.js @@ -1,4 +1,4 @@ -import React, { memo, useCallback, useEffect } from 'react'; +import React, { memo, useCallback, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import Modal from '../common/Modal'; import { Grid, makeStyles, IconButton, Tooltip } from '@material-ui/core'; @@ -33,6 +33,7 @@ const useStyles = makeStyles(theme => ({ export const TrackingModal = memo(({ openModal, onModalClose }) => { const classes = useStyles(); const dispatch = useDispatch(); + const bottomRef = useRef(); const actionList = useSelector(state => state.trackingReducers.project_actions_list); const orderedActionList = (actionList && actionList.sort((a, b) => a.timestamp - b.timestamp)) || []; @@ -43,6 +44,16 @@ export const TrackingModal = memo(({ openModal, onModalClose }) => { } }, [dispatch, openModal]); + const scrollToBottom = () => { + if (bottomRef.current != null) { + bottomRef.current.scrollIntoView({ + behavior: 'auto', + block: 'nearest', + inline: 'nearest' + }); + } + }; + useEffect(() => { loadAllActions(); }, [loadAllActions]); @@ -79,6 +90,12 @@ export const TrackingModal = memo(({ openModal, onModalClose }) => { } })} +
{ + bottomRef.current = el; + scrollToBottom(); + }} + >
diff --git a/js/components/userFeedback/githubApi.js b/js/components/userFeedback/githubApi.js index 38f523f75..76160fd7f 100644 --- a/js/components/userFeedback/githubApi.js +++ b/js/components/userFeedback/githubApi.js @@ -73,16 +73,20 @@ export const createIssue = ({ dispatch(setResponse('')); const rootReducer = getState(); - // const state => state.issueReducers; - const screenshotUrl = await dispatch( - uploadFile({ - data64Based: imageSource.split(',')[1], - formType, - name: 'Screenshot', - extension: EXTENSION.PNG, - raw: true - }) - ); + let screenshotUrl = undefined; + if (imageSource && imageSource !== '') { + // const state => state.issueReducers; + screenshotUrl = await dispatch( + uploadFile({ + data64Based: imageSource.split(',')[1], + formType, + name: 'Screenshot', + extension: EXTENSION.PNG, + raw: true + }) + ); + } + const reducerUrl = await dispatch( uploadFile({ data64Based: Base64.encode(JSON.stringify(rootReducer)), @@ -96,7 +100,7 @@ export const createIssue = ({ body.push('- Reducers: ' + reducerUrl); } - if (screenshotUrl.length > 0) { + if (screenshotUrl && screenshotUrl.length > 0) { body.push('', '![screenshot](' + screenshotUrl + ')'); } diff --git a/js/reducers/tracking/constants.js b/js/reducers/tracking/constants.js index e2389c316..0469200e2 100644 --- a/js/reducers/tracking/constants.js +++ b/js/reducers/tracking/constants.js @@ -125,4 +125,4 @@ export const actionAnnotation = { STAR: 'STAR' }; -export const NUM_OF_SECONDS_TO_IGNORE_MERGE = 5; +export const NUM_OF_SECONDS_TO_IGNORE_MERGE = 2; diff --git a/js/reducers/tracking/dispatchActions.js b/js/reducers/tracking/dispatchActions.js index 07b2ec971..5f07d609f 100644 --- a/js/reducers/tracking/dispatchActions.js +++ b/js/reducers/tracking/dispatchActions.js @@ -94,7 +94,8 @@ import { setIsActionsLoading, setActionsList, setSnapshotImageActionList, - setUndoRedoActionList + setUndoRedoActionList, + setPast } from './actions'; import { api, METHOD } from '../../../js/utils/api'; import { base_url } from '../../components/routes/constants'; @@ -124,6 +125,7 @@ import { setInspirationMoleculeDataList } from '../../components/datasets/redux/actions'; import { selectVectorAndResetCompounds } from '../../../js/reducers/selection/dispatchActions'; +import { ActionCreators as UndoActionCreators } from '../../undoredo/actions' export const addCurrentActionsListToSnapshot = (snapshot, project, nglViewList) => async (dispatch, getState) => { let projectID = project && project.projectID; @@ -980,6 +982,8 @@ const restoreAllSelectionByTypeActions = (moleculesAction, stage, isSelection) = if (data) { if (type === 'ligand') { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true, true)); + } else if (type === 'vector') { + dispatch(addType[type](stage, data, true)); } else { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true)); } @@ -1121,6 +1125,8 @@ const addNewType = (moleculesAction, actionType, type, stage, state, skipTrackin if (data) { if (type === 'ligand') { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true, skipTracking)); + } else if (type === 'vector') { + dispatch(addType[type](stage, data, true)); } else { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], skipTracking)); } @@ -1135,6 +1141,8 @@ const addNewTypeOfAction = (action, type, stage, state, skipTracking = false) => if (data) { if (type === 'ligand') { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true, skipTracking)); + } else if (type === 'vector') { + dispatch(addType[type](stage, data, true)); } else { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], skipTracking)); } @@ -1595,6 +1603,8 @@ const handleAllActionByType = (action, isAdd, stage) => (dispatch, getState) => if (data) { if (type === 'ligand') { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true, true)); + } else if (type === 'vector') { + dispatch(addType[type](stage, data, true)); } else { dispatch(addType[type](stage, data, colourList[data.id % colourList.length], true)); } @@ -1605,7 +1615,7 @@ const handleAllActionByType = (action, isAdd, stage) => (dispatch, getState) => actionItems.forEach(data => { if (data) { - if (type === 'ligand') { + if (type === 'ligand' || type === 'vector') { dispatch(removeType[type](stage, data, true)); } else { dispatch(removeType[type](stage, data, colourList[data.id % colourList.length], true)); @@ -2125,6 +2135,8 @@ const handleMoleculeGroupAction = (action, isSelected, stageSummaryView, majorVi for (const mol of typeGroup) { if (type === 'ligand') { dispatch(addType[type](majorViewStage, mol, colourList[mol.id % colourList.length], true, true)); + } else if (type === 'vector') { + dispatch(addType[type](majorViewStage, mol, true)); } else { dispatch(addType[type](majorViewStage, mol, colourList[mol.id % colourList.length], true)); } @@ -2176,8 +2188,8 @@ const removeNewType = (action, type, stage, state, skipTracking) => dispatch => if (action) { let data = getMolecule(action.object_name, state); if (data) { - if (type === 'ligand') { - dispatch(removeType[type](stage, data, skipTracking)); + if (type === 'ligand' || type === 'vector') { + dispatch(removeType[type](stage, data, skipTracking, false)); } else { dispatch(removeType[type](stage, data, colourList[data.id % colourList.length], skipTracking)); } @@ -2216,23 +2228,26 @@ export const getRedoActionText = () => (dispatch, getState) => { return action?.text ?? ''; }; -export const appendAndSendTrackingActions = trackAction => (dispatch, getState) => { +export const appendAndSendTrackingActions = trackAction => async (dispatch, getState) => { const state = getState(); const isUndoRedoAction = state.trackingReducers.isUndoRedoAction; dispatch(setIsActionTracking(true)); - if (trackAction && trackAction !== null) { const actionList = state.trackingReducers.track_actions_list; const sendActionList = state.trackingReducers.send_actions_list; const mergedActionList = mergeActions(trackAction, [...actionList]); const mergedSendActionList = mergeActions(trackAction, [...sendActionList]); - dispatch(setActionsList(mergedActionList)); - dispatch(setSendActionsList(mergedSendActionList)); - + dispatch(setActionsList(mergedActionList.list)); + dispatch(setSendActionsList(mergedSendActionList.list)); if (isUndoRedoAction === false) { const undoRedoActionList = state.trackingReducers.undo_redo_actions_list; const mergedUndoRedoActionList = mergeActions(trackAction, [...undoRedoActionList]); - dispatch(setUndoRedoActionList(mergedUndoRedoActionList)); + if (mergedActionList.merged) { + dispatch(setUndoRedoActionList(mergedUndoRedoActionList.list)); + dispatch(UndoActionCreators.removeLastPast()); + } else { + dispatch(setUndoRedoActionList(mergedUndoRedoActionList.list)); + } } } dispatch(setIsActionTracking(false)); @@ -2240,6 +2255,7 @@ export const appendAndSendTrackingActions = trackAction => (dispatch, getState) }; export const mergeActions = (trackAction, list) => { + let merged = false; if (needsToBeMerged(trackAction)) { let newList = []; if (list.length > 0) { @@ -2248,16 +2264,18 @@ export const mergeActions = (trackAction, list) => { trackAction.oldSetting = lastEntry.oldSetting; trackAction.text = trackAction.getText(); newList = [...list.slice(0, list.length - 1), trackAction]; + merged = true; } else { newList = [...list, trackAction]; } } else { newList.push(trackAction); } - return newList; + return {merged: merged, list: newList}; } else { - return [...list, trackAction]; + return {merged: merged, list: [...list, trackAction]}; } + // return {merged: merged, list: [...list, trackAction]}; }; const needsToBeMerged = trackAction => { diff --git a/js/reducers/tracking/trackingActions.js b/js/reducers/tracking/trackingActions.js index bc7334478..e7645ac77 100644 --- a/js/reducers/tracking/trackingActions.js +++ b/js/reducers/tracking/trackingActions.js @@ -949,7 +949,7 @@ export const findTrackAction = (action, state) => { object_name: 'NGL', oldSetting: oldSetting, newSetting: newSetting, - text: `Color of NGL ${actionDescription.CHANGED} from value: ${oldSetting} to value: ${newSetting}` + text: `Color of NGL ${actionDescription.CHANGED} to value: ${newSetting}` }; } else if (action.type === nglConstants.SET_CLIP_NEAR) { let oldSetting = action.payload.oldValue; @@ -975,7 +975,7 @@ export const findTrackAction = (action, state) => { this.newSetting ); }, - text: `Clip near of NGL ${actionDescription.CHANGED} from value: ${oldSetting} to value: ${newSetting}` + text: `Clip near of NGL ${actionDescription.CHANGED} to value: ${newSetting}` }; } else if (action.type === nglConstants.SET_CLIP_FAR) { let oldSetting = action.payload.oldValue; @@ -1001,7 +1001,7 @@ export const findTrackAction = (action, state) => { this.newSetting ); }, - text: `Clip far of NGL ${actionDescription.CHANGED} from value: ${oldSetting} to value: ${newSetting}` + text: `Clip far of NGL ${actionDescription.CHANGED} to value: ${newSetting}` }; } else if (action.type === nglConstants.SET_CLIP_DIST) { let oldSetting = action.payload.oldValue; @@ -1027,7 +1027,7 @@ export const findTrackAction = (action, state) => { this.newSetting ); }, - text: `Clip dist of NGL ${actionDescription.CHANGED} from value: ${oldSetting} to value: ${newSetting}` + text: `Clip dist of NGL ${actionDescription.CHANGED} to value: ${newSetting}` }; } else if (action.type === nglConstants.SET_FOG_NEAR) { let oldSetting = action.payload.oldValue; @@ -1053,7 +1053,7 @@ export const findTrackAction = (action, state) => { this.newSetting ); }, - text: `For near of NGL ${actionDescription.CHANGED} from value: ${oldSetting} to value: ${newSetting}` + text: `Fog near of NGL ${actionDescription.CHANGED} to value: ${newSetting}` }; } else if (action.type === nglConstants.SET_FOG_FAR) { let oldSetting = action.payload.oldValue; @@ -1079,7 +1079,7 @@ export const findTrackAction = (action, state) => { this.newSetting ); }, - text: `For far of NGL ${actionDescription.CHANGED} from value: ${oldSetting} to value: ${newSetting}` + text: `Fog far of NGL ${actionDescription.CHANGED} to value: ${newSetting}` }; } } diff --git a/js/reducers/tracking/trackingReducers.js b/js/reducers/tracking/trackingReducers.js index 20c6bb58e..ac65c99c7 100644 --- a/js/reducers/tracking/trackingReducers.js +++ b/js/reducers/tracking/trackingReducers.js @@ -1,5 +1,6 @@ import { constants } from './constants'; -import undoable, { includeAction } from 'redux-undo'; +import {undoable } from '../../undoredo/reducer'; +import { includeAction } from '../../undoredo/helpers'; export const INITIAL_STATE = { track_actions_list: [], @@ -111,7 +112,7 @@ export function trackingReducers(state = INITIAL_STATE, action = {}) { return Object.assign({}, state, { trackingImageSource: action.payload }); - + case constants.RESET_TRACKING_STATE: return INITIAL_STATE; diff --git a/js/undoredo/actions.js b/js/undoredo/actions.js new file mode 100644 index 000000000..33834025b --- /dev/null +++ b/js/undoredo/actions.js @@ -0,0 +1,33 @@ +export const ActionTypes = { + UNDO: '@@redux-undo/UNDO', + REDO: '@@redux-undo/REDO', + JUMP_TO_FUTURE: '@@redux-undo/JUMP_TO_FUTURE', + JUMP_TO_PAST: '@@redux-undo/JUMP_TO_PAST', + JUMP: '@@redux-undo/JUMP', + CLEAR_HISTORY: '@@redux-undo/CLEAR_HISTORY', + REMOVE_LAST_PAST: '@@redux-undo/REMOVE_LAST_PAST' +} + +export const ActionCreators = { + undo () { + return { type: ActionTypes.UNDO } + }, + redo () { + return { type: ActionTypes.REDO } + }, + jumpToFuture (index) { + return { type: ActionTypes.JUMP_TO_FUTURE, index } + }, + jumpToPast (index) { + return { type: ActionTypes.JUMP_TO_PAST, index } + }, + jump (index) { + return { type: ActionTypes.JUMP, index } + }, + clearHistory () { + return { type: ActionTypes.CLEAR_HISTORY } + }, + removeLastPast() { + return {type: ActionTypes.REMOVE_LAST_PAST} + } +} diff --git a/js/undoredo/debug.js b/js/undoredo/debug.js new file mode 100644 index 000000000..739806dee --- /dev/null +++ b/js/undoredo/debug.js @@ -0,0 +1,90 @@ +let __DEBUG__ +let displayBuffer + +const colors = { + prevState: '#9E9E9E', + action: '#03A9F4', + nextState: '#4CAF50' +} + +/* istanbul ignore next: debug messaging is not tested */ +function initBuffer () { + displayBuffer = { + header: [], + prev: [], + action: [], + next: [], + msgs: [] + } +} + +/* istanbul ignore next: debug messaging is not tested */ +function printBuffer () { + const { header, prev, next, action, msgs } = displayBuffer + if (console.group) { + console.groupCollapsed(...header) + console.log(...prev) + console.log(...action) + console.log(...next) + console.log(...msgs) + console.groupEnd() + } else { + console.log(...header) + console.log(...prev) + console.log(...action) + console.log(...next) + console.log(...msgs) + } +} + +/* istanbul ignore next: debug messaging is not tested */ +function colorFormat (text, color, obj) { + return [ + `%c${text}`, + `color: ${color}; font-weight: bold`, + obj + ] +} + +/* istanbul ignore next: debug messaging is not tested */ +function start (action, state) { + initBuffer() + if (__DEBUG__) { + if (console.group) { + displayBuffer.header = ['%credux-undo', 'font-style: italic', 'action', action.type] + displayBuffer.action = colorFormat('action', colors.action, action) + displayBuffer.prev = colorFormat('prev history', colors.prevState, state) + } else { + displayBuffer.header = ['redux-undo action', action.type] + displayBuffer.action = ['action', action] + displayBuffer.prev = ['prev history', state] + } + } +} + +/* istanbul ignore next: debug messaging is not tested */ +function end (nextState) { + if (__DEBUG__) { + if (console.group) { + displayBuffer.next = colorFormat('next history', colors.nextState, nextState) + } else { + displayBuffer.next = ['next history', nextState] + } + printBuffer() + } +} + +/* istanbul ignore next: debug messaging is not tested */ +function log (...args) { + if (__DEBUG__) { + displayBuffer.msgs = displayBuffer.msgs + .concat([...args, '\n']) + } +} + +/* istanbul ignore next: debug messaging is not tested */ +function set (debug) { + __DEBUG__ = debug +} + +export { set, start, end, log } diff --git a/js/undoredo/helpers.js b/js/undoredo/helpers.js new file mode 100644 index 000000000..a8a9532f3 --- /dev/null +++ b/js/undoredo/helpers.js @@ -0,0 +1,57 @@ +// parseActions helper: takes a string (or array) +// and makes it an array if it isn't yet +export function parseActions (rawActions, defaultValue = []) { + if (Array.isArray(rawActions)) { + return rawActions + } else if (typeof rawActions === 'string') { + return [rawActions] + } + return defaultValue +} + +// isHistory helper: check for a valid history object +export function isHistory (history) { + return typeof history.present !== 'undefined' && + typeof history.future !== 'undefined' && + typeof history.past !== 'undefined' && + Array.isArray(history.future) && + Array.isArray(history.past) +} + +// includeAction helper: whitelist actions to be added to the history +export function includeAction (rawActions) { + const actions = parseActions(rawActions) + return (action) => actions.indexOf(action.type) >= 0 +} + +// excludeAction helper: blacklist actions from being added to the history +export function excludeAction (rawActions) { + const actions = parseActions(rawActions) + return (action) => actions.indexOf(action.type) < 0 +} + +// combineFilters helper: combine multiple filters to one +export function combineFilters (...filters) { + return filters.reduce((prev, curr) => + (action, currentState, previousHistory) => + prev(action, currentState, previousHistory) && + curr(action, currentState, previousHistory) + , () => true) +} + +export function groupByActionTypes (rawActions) { + const actions = parseActions(rawActions) + return (action) => actions.indexOf(action.type) >= 0 ? action.type : null +} + +export function newHistory (past, present, future, group = null) { + return { + past, + present, + future, + group, + _latestUnfiltered: present, + index: past.length, + limit: past.length + future.length + 1 + } +} diff --git a/js/undoredo/index.js b/js/undoredo/index.js new file mode 100644 index 000000000..a62f460ce --- /dev/null +++ b/js/undoredo/index.js @@ -0,0 +1,8 @@ +export { ActionTypes, ActionCreators } from './actions' +export { + parseActions, isHistory, + includeAction, excludeAction, + combineFilters, groupByActionTypes, newHistory +} from './helpers' + +export { default } from './reducer' diff --git a/js/undoredo/reducer.js b/js/undoredo/reducer.js new file mode 100644 index 000000000..8e1da2517 --- /dev/null +++ b/js/undoredo/reducer.js @@ -0,0 +1,274 @@ +import * as debug from './debug' +import { ActionTypes } from './actions' +import { parseActions, isHistory, newHistory } from './helpers' + +// createHistory +function createHistory (state, ignoreInitialState) { + // ignoreInitialState essentially prevents the user from undoing to the + // beginning, in the case that the undoable reducer handles initialization + // in a way that can't be redone simply + const history = newHistory([], state, []) + if (ignoreInitialState) { + history._latestUnfiltered = null + } + return history +} + +function removeLastPast(history) { + const { past, present, future } = history; + + const newPast = [...past]; + newPast.pop(); + + return newHistory(newPast, present, future) +} + +// insert: insert `state` into history, which means adding the current state +// into `past`, setting the new `state` as `present` and erasing +// the `future`. +function insert (history, state, limit, group) { + const lengthWithoutFuture = history.past.length + 1 + + debug.log('inserting', state) + debug.log('new free: ', limit - lengthWithoutFuture) + + const { past, _latestUnfiltered } = history + const isHistoryOverflow = limit && limit <= lengthWithoutFuture + + const pastSliced = past.slice(isHistoryOverflow ? 1 : 0) + const newPast = _latestUnfiltered != null + ? [ + ...pastSliced, + _latestUnfiltered + ] : pastSliced + + return newHistory(newPast, state, [], group) +} + +// jumpToFuture: jump to requested index in future history +function jumpToFuture (history, index) { + if (index < 0 || index >= history.future.length) return history + + const { past, future, _latestUnfiltered } = history + + const newPast = [...past, _latestUnfiltered, ...future.slice(0, index)] + const newPresent = future[index] + const newFuture = future.slice(index + 1) + + return newHistory(newPast, newPresent, newFuture) +} + +// jumpToPast: jump to requested index in past history +function jumpToPast (history, index) { + if (index < 0 || index >= history.past.length) return history + + const { past, future, _latestUnfiltered } = history + + const newPast = past.slice(0, index) + const newFuture = [...past.slice(index + 1), _latestUnfiltered, ...future] + const newPresent = past[index] + + return newHistory(newPast, newPresent, newFuture) +} + +// jump: jump n steps in the past or forward +function jump (history, n) { + if (n > 0) return jumpToFuture(history, n - 1) + if (n < 0) return jumpToPast(history, history.past.length + n) + return history +} + +// helper to dynamically match in the reducer's switch-case +function actionTypeAmongClearHistoryType (actionType, clearHistoryType) { + return clearHistoryType.indexOf(actionType) > -1 ? actionType : !actionType +} + +// redux-undo higher order reducer +export function undoable (reducer, rawConfig = {}) { + debug.set(rawConfig.debug) + + const config = { + limit: undefined, + filter: () => true, + groupBy: () => null, + undoType: ActionTypes.UNDO, + redoType: ActionTypes.REDO, + jumpToPastType: ActionTypes.JUMP_TO_PAST, + jumpToFutureType: ActionTypes.JUMP_TO_FUTURE, + jumpType: ActionTypes.JUMP, + removeLastPast: ActionTypes.REMOVE_LAST_PAST, + neverSkipReducer: false, + ignoreInitialState: false, + syncFilter: false, + + ...rawConfig, + + initTypes: parseActions(rawConfig.initTypes, ['@@redux-undo/INIT']), + clearHistoryType: parseActions( + rawConfig.clearHistoryType, + [ActionTypes.CLEAR_HISTORY] + ) + } + + // Allows the user to call the reducer with redux-undo specific actions + const skipReducer = config.neverSkipReducer + ? (res, action, ...slices) => ({ + ...res, + present: reducer(res.present, action, ...slices) + }) + : (res) => res + + let initialState + return (state = initialState, action = {}, ...slices) => { + debug.start(action, state) + + let history = state + if (!initialState) { + debug.log('history is uninitialized') + + if (state === undefined) { + const createHistoryAction = { type: '@@redux-undo/CREATE_HISTORY' } + const start = reducer(state, createHistoryAction, ...slices) + + history = createHistory( + start, + config.ignoreInitialState + ) + + debug.log('do not set initialState on probe actions') + debug.end(history) + return history + } else if (isHistory(state)) { + history = initialState = config.ignoreInitialState + ? state : newHistory( + state.past, + state.present, + state.future + ) + debug.log( + 'initialHistory initialized: initialState is a history', + initialState + ) + } else { + history = initialState = createHistory( + state, + config.ignoreInitialState + ) + debug.log( + 'initialHistory initialized: initialState is not a history', + initialState + ) + } + } + + let res + switch (action.type) { + case undefined: + return history + + case config.undoType: + res = jump(history, -1) + debug.log('perform undo') + debug.end(res) + return skipReducer(res, action, ...slices) + + case config.redoType: + res = jump(history, 1) + debug.log('perform redo') + debug.end(res) + return skipReducer(res, action, ...slices) + + case config.jumpToPastType: + res = jumpToPast(history, action.index) + debug.log(`perform jumpToPast to ${action.index}`) + debug.end(res) + return skipReducer(res, action, ...slices) + + case config.jumpToFutureType: + res = jumpToFuture(history, action.index) + debug.log(`perform jumpToFuture to ${action.index}`) + debug.end(res) + return skipReducer(res, action, ...slices) + + case config.jumpType: + res = jump(history, action.index) + debug.log(`perform jump to ${action.index}`) + debug.end(res) + return skipReducer(res, action, ...slices) + + case config.removeLastPast: + res = removeLastPast(history); + return skipReducer(res, action, ...slices); + + case actionTypeAmongClearHistoryType(action.type, config.clearHistoryType): + res = createHistory(history.present, config.ignoreInitialState) + debug.log('perform clearHistory') + debug.end(res) + return skipReducer(res, action, ...slices) + + default: + res = reducer( + history.present, + action, + ...slices + ) + + if (config.initTypes.some((actionType) => actionType === action.type)) { + debug.log('reset history due to init action') + debug.end(initialState) + return initialState + } + + if (history._latestUnfiltered === res) { + // Don't handle this action. Do not call debug.end here, + // because this action should not produce side effects to the console + return history + } + + /* eslint-disable-next-line no-case-declarations */ + const filtered = typeof config.filter === 'function' && !config.filter( + action, + res, + history + ) + + if (filtered) { + // if filtering an action, merely update the present + const filteredState = newHistory( + history.past, + res, + history.future, + history.group + ) + if (!config.syncFilter) { + filteredState._latestUnfiltered = history._latestUnfiltered + } + debug.log('filter ignored action, not storing it in past') + debug.end(filteredState) + return filteredState + } + + /* eslint-disable-next-line no-case-declarations */ + const group = config.groupBy(action, res, history) + if (group != null && group === history.group) { + // if grouping with the previous action, only update the present + const groupedState = newHistory( + history.past, + res, + history.future, + history.group + ) + debug.log('groupBy grouped the action with the previous action') + debug.end(groupedState) + return groupedState + } + + // If the action wasn't filtered or grouped, insert normally + history = insert(history, res, config.limit, group) + + debug.log('inserted new state into history') + debug.end(history) + return history + } + } +} diff --git a/package.json b/package.json index 10cefd9c7..e100f5516 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fragalysis-frontend", - "version": "0.9.54", + "version": "0.9.57", "description": "Frontend for fragalysis", "main": "webpack.config.js", "scripts": { @@ -72,7 +72,6 @@ "redux-devtools-extension": "^2.13.8", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", - "redux-undo": "^1.0.1", "reselect": "^4.0.0", "style-loader": "^1.0.0", "typeface-roboto": "^0.0.75",