From 7e6c4addcea651c100251ccbd518e83974a9931b Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 21 Jan 2021 13:36:38 -0500 Subject: [PATCH] Goal loading cleanup; Fix goal history reversal bug. (#925) Cleanup and refactor: * Move currentUser check to within userEditFetch. * Remove unused GoalSelectorScroll. * Remove useless goal update in CharacterInventoryActions. * Replace custom functions getIndexInHistory and findGoalByName with use of .find(). * Make consistent usage of async/await with goal actions. Bug-fix: Remove .reverse() on the goal history in the state, and only reverse what is given to the history display. Resolves issues #243, #698, #795. --- src/components/App/DefaultState.tsx | 4 - .../GoalSelectorScroll/GoalSelectorAction.tsx | 27 -- .../GoalSelectorReducer.tsx | 37 -- .../GoalSelectorScroll/GoalSelectorScroll.tsx | 424 ------------------ .../GoalTimeline/GoalSelectorScroll/index.tsx | 32 -- .../tests/GoalSelectorAction.test.tsx | 27 -- .../tests/GoalSelectorReducer.test.tsx | 58 --- .../tests/GoalSelectorScroll.test.tsx | 237 ---------- .../GoalSelectorScroll.test.tsx.snap | 321 ------------- .../GoalTimeline/GoalTimelineComponent.tsx | 17 +- src/components/GoalTimeline/GoalsActions.tsx | 155 ++----- src/components/GoalTimeline/index.tsx | 2 +- .../tests/GoalTimelineActions.test.tsx | 361 +++++++-------- .../tests/GoalTimelineComponent.test.tsx | 12 - .../CreateProject/CreateProjectActions.tsx | 6 +- .../CharacterInventoryActions.tsx | 17 +- .../tests/CharacterInventoryActions.test.tsx | 32 +- src/rootReducer.tsx | 2 - src/types/goals.tsx | 11 - src/types/index.tsx | 3 +- 20 files changed, 233 insertions(+), 1552 deletions(-) delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorAction.tsx delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorReducer.tsx delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorScroll.tsx delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/index.tsx delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorAction.test.tsx delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorReducer.test.tsx delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorScroll.test.tsx delete mode 100644 src/components/GoalTimeline/GoalSelectorScroll/tests/__snapshots__/GoalSelectorScroll.test.tsx.snap diff --git a/src/components/App/DefaultState.tsx b/src/components/App/DefaultState.tsx index 416d94ecb0..094689a6cb 100644 --- a/src/components/App/DefaultState.tsx +++ b/src/components/App/DefaultState.tsx @@ -4,7 +4,6 @@ import { defaultState as reviewEntriesState } from "../../goals/ReviewEntries/Re import { defaultProject } from "../../types/project"; import { defaultState as loginState } from "../Login/LoginReducer"; import { defaultState as goalTimelineState } from "../GoalTimeline/DefaultState"; -import { defaultState as goalSelectorState } from "../GoalTimeline/GoalSelectorScroll/GoalSelectorReducer"; import { defaultState as passwordResetState } from "../PasswordReset/reducer"; import { defaultState as exportProjectState } from "../ProjectExport/ExportProjectReducer"; import { defaultState as createProjectState } from "../ProjectScreen/CreateProject/CreateProjectReducer"; @@ -46,9 +45,6 @@ export const defaultState = { }, //general cleanup tools - goalSelectorState: { - ...goalSelectorState, - }, goalsState: { ...goalTimelineState, }, diff --git a/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorAction.tsx b/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorAction.tsx deleted file mode 100644 index bdef3e34c0..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorAction.tsx +++ /dev/null @@ -1,27 +0,0 @@ -export const SELECT_ACTION = "PRESS_BUTTON"; -export type SELECT_ACTION = typeof SELECT_ACTION; - -export const MOUSE_ACTION = "MOUSE_ACTION"; -export type MOUSE_ACTION = typeof MOUSE_ACTION; - -//action types -export interface GoalScrollAction { - type: SELECT_ACTION | MOUSE_ACTION; - payload: number; -} - -export function scrollSelectorIndexAction( - selectedIndex: number -): GoalScrollAction { - return { - type: SELECT_ACTION, - payload: selectedIndex, - }; -} - -export function scrollSelectorMouseAction(mouseX: number): GoalScrollAction { - return { - type: MOUSE_ACTION, - payload: mouseX, - }; -} diff --git a/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorReducer.tsx b/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorReducer.tsx deleted file mode 100644 index 32aa326334..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorReducer.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { StoreAction, StoreActions } from "../../../rootActions"; -import { GoalSelectorState } from "../../../types/goals"; -import { - GoalScrollAction, - MOUSE_ACTION, - SELECT_ACTION, -} from "./GoalSelectorAction"; - -export const defaultState: GoalSelectorState = { - selectedIndex: 0, - allPossibleGoals: [], - mouseX: 0, - lastIndex: 0, -}; - -export const goalSelectReducer = ( - state: GoalSelectorState | undefined, - action: StoreAction | GoalScrollAction -): GoalSelectorState => { - if (!state) return defaultState; - switch (action.type) { - case SELECT_ACTION: - return { - ...state, - selectedIndex: action.payload, - }; - case MOUSE_ACTION: - return { - ...state, - mouseX: action.payload, - }; - case StoreActions.RESET: - return defaultState; - default: - return state; - } -}; diff --git a/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorScroll.tsx b/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorScroll.tsx deleted file mode 100644 index 1a9158a981..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/GoalSelectorScroll.tsx +++ /dev/null @@ -1,424 +0,0 @@ -import Card from "@material-ui/core/Card"; -import { Button, CardContent, Typography } from "@material-ui/core"; -import React, { ReactElement } from "react"; -import { - LocalizeContextProps, - Translate, - withLocalize, -} from "react-localize-redux"; - -import { Goal, GoalName } from "../../../types/goals"; -import { styleAddendum } from "../../../types/theme"; -import ContextMenu from "../../ContextMenu/ContextMenu"; - -const CLICK_SENSITIVITY: number = 10; - -// Defines relations of scroller to allow resizing -const NUM_PANES: number = 5; // Number of panes present in the ui. Must be odd -export const WIDTH: number = Math.floor(100 / (NUM_PANES + 2)); // Width of each card -const SCALE_FACTOR_FOR_DESELECTED = 0.9; // The percent of regular size that deselected cards shrink to - -// Constants derived from scroller relations -const AMT_OF_PADDING: number = Math.ceil(NUM_PANES / 2); // padding frames added to the sides -export const WRAP_AROUND_THRESHHOLD: number = WIDTH / 2; // The amount of scroll-over needed before tripping the wrap -const DESELECTED_WIDTH: number = WIDTH * SCALE_FACTOR_FOR_DESELECTED; // Width of each not-selected card - -// Action names -const SELECT = "selectstart"; -const M_DUR = "mousemove"; -const M_END = "mouseup"; - -// classNames -const SCROLL_PANE: string = "scrollPane"; -const SCROLL_CONT: string = "scrollCont"; -export const SCROLL_CARD: string = "scrollCard"; // Exported for testing - -// Style constants -const VIEW_WIDTH: number = WIDTH * NUM_PANES; // Width of the screen's view - -// Keyboard constants -const LEFT_KEY: string = "ArrowLeft"; -const RIGHT_KEY: string = "ArrowRight"; -const ENTER_KEY: string = "Enter"; - -export interface GoalSelectorScrollProps { - allPossibleGoals: Goal[]; - selectedIndex: number; - mouseX: number; - lastIndex: number; - swapSelectedIndex: (ndx: number) => void; - swapMouseX: (iX: number) => void; - handleChange: (value: GoalName) => void; -} - -export class GoalSelectorScroll extends React.Component< - GoalSelectorScrollProps & LocalizeContextProps -> { - // Constants used in scrolling mechanics + aesthetics - readonly LENGTH: number; - readonly TICKER_WIDTH: number; - readonly LEAD_PADDING: [string, number, number][]; - readonly END_PADDING: [string, number, number][]; - - // The set of styles used to make this UI work - readonly style = { - container: { - padding: "2vw", - display: "flex", - flexWrap: "nowrap", - overflow: "hidden", - }, - pane: { - padding: "2vw", - userselect: "none", - display: "flex", - flexWrap: "nowrap", - overflow: "hidden", - width: VIEW_WIDTH + "vw", - }, - scroll: { - flexWrap: "nowrap", - display: "flex", - }, - selectedCard: { - width: WIDTH + "vw", - }, - inactiveCard: { - ...styleAddendum.inactive, - width: DESELECTED_WIDTH + "vw", - margin: (WIDTH - DESELECTED_WIDTH) / 2 + "vw", - }, - }; - - scrollRef: any; - mouseStart: number; - detectMouse: boolean; - - // Menu anchor: where the right-click menu appears - anchorElement: Element | null; - - constructor(props: GoalSelectorScrollProps & LocalizeContextProps) { - super(props); - - // Set LENGTH, TICKER_WIDTH, and padding arrays - this.LENGTH = props.allPossibleGoals.length; - this.TICKER_WIDTH = WIDTH * (this.LENGTH + AMT_OF_PADDING * 2 - 1); - this.LEAD_PADDING = props.allPossibleGoals - .slice(this.LENGTH - AMT_OF_PADDING, this.LENGTH) - .map((element: Goal, index: number) => { - return [ - element.name, - index + this.LENGTH - AMT_OF_PADDING, - index + this.LENGTH - AMT_OF_PADDING, - ]; - }); - this.END_PADDING = props.allPossibleGoals - .slice(0, AMT_OF_PADDING) - .map((element: Goal, index: number) => { - return [element.name, index, index]; - }); - - // Bind so as to be able to be used as event listeners/map function - this.scrollDur = this.scrollDur.bind(this); - this.scrollEnd = this.scrollEnd.bind(this); - this.mapScrollCard = this.mapScrollCard.bind(this); - this.mapWraparoundCard = this.mapWraparoundCard.bind(this); - this.keyboardListener = this.keyboardListener.bind(this); - this.resizeListener = this.resizeListener.bind(this); - - // Create scroll ref so that we can programmatically scroll - this.scrollRef = React.createRef(); - - // Used in mouse detection - this.mouseStart = 0; - this.detectMouse = true; - - // Menu - this.anchorElement = null; - } - - // Updates the position of the ticker upon mounting to override odd FireFox behavior - componentDidMount() { - this.centerUI(this.props.selectedIndex); - window.addEventListener("keydown", this.keyboardListener); - window.addEventListener("resize", this.resizeListener); - } - - componentWillUnmount() { - window.removeEventListener("keydown", this.keyboardListener); - window.removeEventListener("resize", this.resizeListener); - } - - // Chooses an index based on the screen's current scroll - selectNewIndex() { - let w: number = this.getScroll().scrollLeft / percentToPixels(WIDTH) - 1; - if (w < 0) w = 0; - return Math.round(w); - } - - // Action handlers ============================================================================================ - - // Begin a scroll; creates additional listeners to allow it to be dragged outside of the narrow strip of scrolling pane. - scrollStart(event: React.MouseEvent) { - window.addEventListener(SELECT, this.block); - window.addEventListener(M_DUR, this.scrollDur); - window.addEventListener(M_END, this.scrollEnd); - this.props.swapMouseX(event.screenX); - this.mouseStart = event.screenX; - } - - // Track mouse during a scroll - scrollDur(event: MouseEvent) { - if (this.detectMouse) { - let newIndex: number; - this.scrollFreeform(this.props.mouseX - event.screenX); - this.props.swapMouseX(event.screenX); - - newIndex = this.selectNewIndex(); - if (newIndex !== this.props.selectedIndex) - this.props.swapSelectedIndex(newIndex); - } - // Detect only every other mouseMove event - this.detectMouse = !this.detectMouse; - } - - // End the scroll, remove listeners, lock into proper position - scrollEnd(event: MouseEvent) { - window.removeEventListener(SELECT, this.block); - window.removeEventListener(M_DUR, this.scrollDur); - window.removeEventListener(M_END, this.scrollEnd); - this.scrollLockNdx(this.selectNewIndex()); - } - - // Prevent selection of text while scrolling - block(event: Event) { - event.preventDefault(); - } - - // Handle clicks for the cards - cardHandleClick(event: React.MouseEvent, index: number) { - // Avoid detecting right-click - if (event.type === "click") { - if (Math.abs(this.mouseStart - this.props.mouseX) < CLICK_SENSITIVITY) - if (this.props.selectedIndex === index) - this.props.handleChange(this.props.allPossibleGoals[index].name); - else this.scrollLockNdx(index); - } - } - - // Adds the keyboard listener - keyboardListener(event: KeyboardEvent) { - if (event.key === LEFT_KEY) { - this.scrollLeft(); - } else if (event.key === RIGHT_KEY) { - this.scrollRight(); - } else if (event.key === ENTER_KEY) { - this.props.handleChange( - this.props.allPossibleGoals[this.props.selectedIndex].name - ); - } - } - - // Adds the resize listener - resizeListener(event: UIEvent) { - this.centerUI(this.props.selectedIndex); - } - - // Scrolling mechanics =================================================================================== - - // Scroll left one goal (w/ wrap) - scrollLeft(): void { - let newNdx = this.props.selectedIndex - 1; - this.scrollLockNdx( - newNdx < 0 ? this.props.allPossibleGoals.length - 1 : newNdx - ); - } - - // Scroll right one goal (w/ wrap) - scrollRight(): void { - let newNdx = - (this.props.selectedIndex + 1) % this.props.allPossibleGoals.length; - this.scrollLockNdx(newNdx); - } - - // Set this.props.selectedIndex and the current centered goal to a specified index - scrollLockNdx(newIndex: number): void { - this.props.swapSelectedIndex(newIndex); - this.centerUI(newIndex); - } - - // Centers the UI on the selected goal - centerUI(centerIndex: number) { - this.getScroll().scrollLeft = percentToPixels(WIDTH) * (centerIndex + 1); - } - - // Scroll the pane freely w/o locking on to goals - scrollFreeform(amount: number): void { - var el: HTMLElement = this.getScroll(); - var newScrollLeft: number = el.scrollLeft + amount; - var threshhold: number = percentToPixels(WRAP_AROUND_THRESHHOLD); - - if (newScrollLeft < threshhold) - newScrollLeft = - percentToPixels(this.TICKER_WIDTH) - - percentToPixels(VIEW_WIDTH) + - threshhold - - 1; - else if ( - newScrollLeft > - percentToPixels(this.TICKER_WIDTH) - - percentToPixels(VIEW_WIDTH) + - threshhold - ) - newScrollLeft = threshhold + 1; - el.scrollLeft = newScrollLeft; - } - - // Returns the scroller; shorthand + facilitates testing in an environment where refs fail - getScroll() { - return this.scrollRef.current; - } - - // Rendering functions ======================================================================================== - // Create the cards used to display the goals - mapScrollCard(goal: Goal, index: number): ReactElement { - return ( - { - this.cardHandleClick(event, index); - }} - onContextMenu={(event: React.MouseEvent) => { - this.cardHandleClick(event, index); - }} - > - - - - - - - { - this.props.handleChange( - this.props.allPossibleGoals[index].name - ); - }, - ], - ]} - /> - - ); - } - - // Add a dummy card - mapWraparoundCard(dummyGoal: [string, number, number], index: number) { - return ( - { - this.cardHandleClick(event, dummyGoal[1]); - }} - onContextMenu={(event: React.MouseEvent) => { - this.cardHandleClick(event, dummyGoal[1]); - }} - > - - - - - - - { - this.props.handleChange( - this.props.allPossibleGoals[dummyGoal[2]].name - ); - }, - ], - ]} - /> - - ); - } - - // Get the style for the cards based on its index - chooseStyle(cardIndex: number) { - if (cardIndex === this.props.selectedIndex) return this.style.selectedCard; - else return this.style.inactiveCard; - } - - render() { - return ( -
{ - this.scrollStart(event); - }} - > - {/* Scroll left button */} - - {/* Scroll pane */} -
-
- {/* This is set up without lambdas because, as of the writing of this code, - usage of lambdas completely broke the ui's scrolling for unknown reasons */} - {this.LEAD_PADDING.map(this.mapWraparoundCard)} - {this.props.allPossibleGoals.map(this.mapScrollCard)} - {this.END_PADDING.map(this.mapWraparoundCard)} -
-
- {/* Scroll right button */} - -
- ); - } -} - -export function percentToPixels(scaleValue: number) { - return (scaleValue / 100) * window.innerWidth; -} - -export default withLocalize(GoalSelectorScroll); diff --git a/src/components/GoalTimeline/GoalSelectorScroll/index.tsx b/src/components/GoalTimeline/GoalSelectorScroll/index.tsx deleted file mode 100644 index d2b7a20166..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { connect } from "react-redux"; - -import { StoreState } from "../../../types"; -import { StoreStateDispatch } from "../../../types/actions"; -import { GoalSelectorState } from "../../../types/goals"; -import { - scrollSelectorIndexAction, - scrollSelectorMouseAction, -} from "./GoalSelectorAction"; -import GoalSelectorScroll from "./GoalSelectorScroll"; - -export function mapStateToProps(state: StoreState): GoalSelectorState { - return { - allPossibleGoals: state.goalsState.allPossibleGoals, - selectedIndex: state.goalSelectorState.selectedIndex, - mouseX: state.goalSelectorState.mouseX, - lastIndex: state.goalsState.allPossibleGoals.length - 1, - }; -} - -export function mapDispatchToProps(dispatch: StoreStateDispatch) { - return { - swapSelectedIndex: (ndx: number) => { - dispatch(scrollSelectorIndexAction(ndx)); - }, - swapMouseX: (iX: number) => { - dispatch(scrollSelectorMouseAction(iX)); - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(GoalSelectorScroll); diff --git a/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorAction.test.tsx b/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorAction.test.tsx deleted file mode 100644 index 57c7977e9d..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorAction.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { - GoalScrollAction, - MOUSE_ACTION, - SELECT_ACTION, - scrollSelectorIndexAction, - scrollSelectorMouseAction, -} from "../GoalSelectorAction"; - -const VAL = 5; - -describe("Goal select action test", () => { - it("Should create the correct select action", () => { - let result: GoalScrollAction = { - type: SELECT_ACTION, - payload: VAL, - }; - expect(scrollSelectorIndexAction(VAL)).toEqual(result); - }); - - it("Should create the correct mouse action", () => { - let result: GoalScrollAction = { - type: MOUSE_ACTION, - payload: VAL, - }; - expect(scrollSelectorMouseAction(VAL)).toEqual(result); - }); -}); diff --git a/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorReducer.test.tsx b/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorReducer.test.tsx deleted file mode 100644 index 2e123275a1..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorReducer.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { StoreActions, StoreAction } from "../../../../rootActions"; -import { GoalSelectorState } from "../../../../types/goals"; -import { - GoalScrollAction, - MOUSE_ACTION, - SELECT_ACTION, -} from "../GoalSelectorAction"; -import { defaultState, goalSelectReducer } from "../GoalSelectorReducer"; - -const VAL = 5; -const scrollAct: GoalScrollAction = { - type: SELECT_ACTION, - payload: VAL, -}; -const scrollResultStore: GoalSelectorState = { - ...defaultState, - selectedIndex: VAL, -}; -const mouseAct: GoalScrollAction = { - type: MOUSE_ACTION, - payload: VAL, -}; -const mouseResultStore: GoalSelectorState = { - ...defaultState, - mouseX: VAL, -}; - -describe("Testing goal select reducer", () => { - it("Should return defaultState", () => { - expect(goalSelectReducer(undefined, scrollAct)).toEqual(defaultState); - }); - - it("Should return a state with an index of " + VAL, () => { - expect(goalSelectReducer(defaultState, scrollAct)).toEqual( - scrollResultStore - ); - }); - - it("Should return a state with an iX of " + VAL, () => { - expect(goalSelectReducer(defaultState, mouseAct)).toEqual(mouseResultStore); - }); - - it("Should return the passed-in store", () => { - expect( - goalSelectReducer(scrollResultStore, ({ - type: "", - payload: 0, - } as unknown) as GoalScrollAction) - ).toEqual(scrollResultStore); - }); - - it("Should return the default state", () => { - const action: StoreAction = { - type: StoreActions.RESET, - }; - expect(goalSelectReducer(scrollResultStore, action)).toEqual(defaultState); - }); -}); diff --git a/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorScroll.test.tsx b/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorScroll.test.tsx deleted file mode 100644 index 0da52c5835..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/tests/GoalSelectorScroll.test.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import React from "react"; -import { Provider } from "react-redux"; -import renderer, { - ReactTestInstance, - ReactTestRenderer, -} from "react-test-renderer"; -import configureMockStore from "redux-mock-store"; -import thunk from "redux-thunk"; - -import { Goal, GoalName, GoalSelectorState } from "../../../../types/goals"; -import { User } from "../../../../types/user"; -import GoalSelectorScroll from "../"; -import { - GoalScrollAction, - MOUSE_ACTION, - SELECT_ACTION, -} from "../GoalSelectorAction"; -import { - GoalSelectorScroll as GSScroll, - percentToPixels, - WIDTH, - WRAP_AROUND_THRESHHOLD, -} from "../GoalSelectorScroll"; - -const labels = [ - GoalName.CreateStrWordInv, - GoalName.HandleFlags, - GoalName.SpellcheckGloss, -]; - -// Create the mock store -const gsState: GoalSelectorState = createTempState(); -const storeState: any = { - innerWidth: 500, - goalSelectorState: gsState, - goalsState: { - allPossibleGoals: gsState.allPossibleGoals, - }, -}; -const createMockStore = configureMockStore([thunk]); -const store = createMockStore(storeState); - -// Mock the DOM -jest.autoMockOn(); - -// Bypass getScroll relying on refs, which fail in jest testing -var scroller: any = { - scrollLeft: WRAP_AROUND_THRESHHOLD, -}; -GSScroll.prototype.getScroll = jest.fn(() => { - return scroller as HTMLElement; -}); - -// TODO: Should this lint be disabled? -// eslint-disable-next-line no-native-reassign -window = { - ...window, - addEventListener: jest.fn(), - removeEventListener: jest.fn(), -}; - -// Variables used in testing: contain various parts of the UI -var scrollMaster: ReactTestRenderer; -var scrollHandle: ReactTestInstance; - -// Action constants -const select: GoalScrollAction = { - type: SELECT_ACTION, - payload: 0, -}; -const mouse: GoalScrollAction = { - type: MOUSE_ACTION, - payload: 0, -}; - -beforeEach(() => { - // Here, use the act block to be able to render our GoalState into the DOM - // Re-created each time to prevent actions from previous runs from affecting future runs - renderer.act(() => { - scrollMaster = renderer.create( - - - - ); - }); - scrollHandle = scrollMaster.root.findByType(GSScroll); - scroller.scrollLeft = percentToPixels(WRAP_AROUND_THRESHHOLD); - - // Reset store actions - store.clearActions(); -}); - -// Actual tests -describe("Testing the goal selector scroll ui", () => { - test("Basic functions work as expected", () => { - expect(percentToPixels(10)).toEqual(window.innerWidth * 0.1); - }); - - it("Constructs correctly", () => { - // Default snapshot test - snapTest("default view"); - }); - - it("Dispatches ndx to store on navigate left", () => { - let action: GoalScrollAction = { - type: SELECT_ACTION, - payload: 2, - }; - scrollHandle.instance.scrollLeft(); - expect(store.getActions()).toEqual([action]); - }); - - it("Dispatches a ScrollSelectorAct to store on navigate right", () => { - scrollHandle.instance.scrollRight(); - expect(store.getActions()).toEqual([ - { - type: SELECT_ACTION, - payload: 1, - }, - ]); - }); - - it("Dispatches a MouseMoveAct to the store on scrollStart", () => { - scrollHandle.instance.scrollStart({ - screenX: mouse.payload, - }); - expect(store.getActions()).toEqual([mouse]); - - // Remove extra listeners - scrollHandle.instance.scrollEnd({ - screenX: 0, - }); - }); - - it("Dispatches a MouseMoveAct on scrollDur-short stroke", () => { - let shortStroke: GoalScrollAction = { - type: MOUSE_ACTION, - payload: -1, - }; - scrollHandle.instance.scrollDur({ - screenX: shortStroke.payload, - }); - expect(store.getActions()).toEqual([shortStroke]); - }); - - it("Dispatches a MouseMoveAct and a ScrollSelectorAct on scrollDur-long stroke", () => { - let newMouse: GoalScrollAction = { - type: MOUSE_ACTION, - payload: -percentToPixels(WIDTH), - }; - let newSelect: GoalScrollAction = { - type: SELECT_ACTION, - payload: 1, - }; - scrollHandle.instance.scrollDur({ - screenX: newMouse.payload, - }); - expect(store.getActions()).toEqual([newMouse, newSelect]); - }); - - it("Dispatches a MouseMoveAct 1 out of every 2 times scrollDur is called", () => { - let shortStroke: GoalScrollAction = { - type: MOUSE_ACTION, - payload: -1, - }; - for (let i: number = 0; i < 4; i++) - scrollHandle.instance.scrollDur({ - screenX: shortStroke.payload, - }); - expect(store.getActions()).toEqual([shortStroke, shortStroke]); - }); - - it("Dispatches a ScrollSelectorAct on scrollEnd", () => { - scrollHandle.instance.scrollEnd({ - screenX: mouse.payload, - }); - expect(store.getActions()).toEqual([select]); - }); - - it("Calls handleChange on click to the active card", () => { - scrollHandle.instance.cardHandleClick( - { - type: "click", - }, - gsState.selectedIndex - ); - expect(scrollHandle.instance.props.handleChange).toHaveBeenCalledWith( - gsState.allPossibleGoals[gsState.selectedIndex].name - ); - }); - - it("Swaps index on click to a non-active card", () => { - scrollHandle.instance.cardHandleClick( - { - type: "click", - }, - gsState.selectedIndex + 1 - ); - expect(store.getActions()).toEqual([ - { - type: SELECT_ACTION, - payload: gsState.selectedIndex + 1, - }, - ]); - }); -}); - -// Utility functions ---------------------------------------------------------------- - -// Perform a snapshot test -function snapTest(name: string) { - expect(scrollMaster.toJSON()).toMatchSnapshot(); -} - -// Create a usable temporary state, as opposed to a dummy state -function createTempState(): GoalSelectorState { - let tempUser: User = new User("Robbie", "NumberOne", "password"); - let goals: Goal[] = []; - - for (let i: number = 0; i < labels.length; i++) - goals[i] = createGoal(labels[i], tempUser); - - return { - selectedIndex: 0, - allPossibleGoals: [...goals], - mouseX: 0, - lastIndex: 3, - }; -} - -// Creates a Goal with the specified attributes -function createGoal(name: GoalName, user: User): Goal { - const goal: Goal = new Goal(); - goal.name = name; - goal.user = user; - return goal; -} diff --git a/src/components/GoalTimeline/GoalSelectorScroll/tests/__snapshots__/GoalSelectorScroll.test.tsx.snap b/src/components/GoalTimeline/GoalSelectorScroll/tests/__snapshots__/GoalSelectorScroll.test.tsx.snap deleted file mode 100644 index 5c2e3c420c..0000000000 --- a/src/components/GoalTimeline/GoalSelectorScroll/tests/__snapshots__/GoalSelectorScroll.test.tsx.snap +++ /dev/null @@ -1,321 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Testing the goal selector scroll ui Constructs correctly 1`] = ` -
- -
-
-
-
-
- Missing translationId: createStrWordInv.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: handleFlags.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: spellcheckGloss.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: createStrWordInv.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: handleFlags.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: spellcheckGloss.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: createStrWordInv.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: handleFlags.title for language: \${ languageCode } -
-
-
-
-
-
- Missing translationId: spellcheckGloss.title for language: \${ languageCode } -
-
-
-
-
- -
-`; diff --git a/src/components/GoalTimeline/GoalTimelineComponent.tsx b/src/components/GoalTimeline/GoalTimelineComponent.tsx index b5de667a8d..b48aaee27c 100644 --- a/src/components/GoalTimeline/GoalTimelineComponent.tsx +++ b/src/components/GoalTimeline/GoalTimelineComponent.tsx @@ -79,25 +79,12 @@ export default class GoalTimeline extends React.Component< // Given a change event, find which goal the user selected, and choose it // as the next goal to work on. handleChange(name: string) { - let goal: Goal | undefined = this.findGoalByName( - this.props.allPossibleGoals, - name - ); + const goal = this.props.allPossibleGoals.find((goal) => goal.name === name); if (goal) { this.props.chooseGoal(goal); } } - // Search through the list of possible goals, and find which one the user - // selected - findGoalByName(goals: Goal[], name: string): Goal | undefined { - for (var goal of goals) { - if (goal.name === name) { - return goal; - } - } - } - // Creates a list of suggestions, with non-suggested goals at the end and // our main suggestion absent (to be displayed on the suggestions button) createSuggestionData(): Goal[] { @@ -216,7 +203,7 @@ export default class GoalTimeline extends React.Component< { + dispatch(loadUserEdits([])); + await Backend.createUserEdit(); + }; +} + export function asyncLoadExistingUserEdits( projectId: string, userEditId: string ) { return async (dispatch: StoreStateDispatch) => { - await Backend.getUserEditById(projectId, userEditId) - .then((userEdit) => { - let history: Goal[] = convertEditsToArrayOfGoals(userEdit.edits); - dispatch(loadUserEdits(history)); - }) - .catch((err) => { - console.log(err); - }); + const userEdit = await Backend.getUserEditById(projectId, userEditId); + const history = convertEditsToGoals(userEdit.edits); + dispatch(loadUserEdits(history)); }; } export function asyncGetUserEdits() { return async (dispatch: StoreStateDispatch) => { - const user = LocalStorage.getCurrentUser(); const projectId = LocalStorage.getProjectId(); - if (user && projectId) { - const userEditId = getUserEditId(user); + if (projectId) { + const userEditId = getUserEditId(); - if (userEditId !== undefined) { - dispatch(asyncLoadExistingUserEdits(projectId, userEditId)); + if (userEditId) { + await dispatch(asyncLoadExistingUserEdits(projectId, userEditId)); } else { - dispatch(Backend.createUserEdit); + await dispatch(asyncCreateUserEdits()); } } }; @@ -91,22 +89,12 @@ export function asyncGetUserEdits() { export function asyncAddGoalToHistory(goal: Goal) { return async (dispatch: StoreStateDispatch) => { - const user = LocalStorage.getCurrentUser(); - if (user) { - const userEditId = getUserEditId(user); - if (userEditId !== undefined) { - dispatch(asyncLoadGoalData(goal)).then( - (returnedGoal) => (goal = returnedGoal) - ); - await Backend.addGoalToUserEdit(userEditId, goal) - .then((resp) => { - dispatch(addGoalToHistory(goal)); - history.push(`${Path.Goals}/${resp}`); - }) - .catch((err: string) => { - console.log(err); - }); - } + const userEditId = getUserEditId(); + if (userEditId) { + goal = await dispatch(asyncLoadGoalData(goal)); + const goalIndex = await Backend.addGoalToUserEdit(userEditId, goal); + dispatch(addGoalToHistory(goal)); + history.push(`${Path.Goals}/${goalIndex}`); } }; } @@ -128,8 +116,8 @@ export function asyncLoadGoalData(goal: Goal) { export function asyncAdvanceStep() { return async (dispatch: StoreStateDispatch, getState: () => StoreState) => { - let historyState: GoalHistoryState = getState().goalsState.historyState; - let goal: Goal = historyState.history[historyState.history.length - 1]; + const goalHistory = getState().goalsState.historyState.history; + const goal = goalHistory[goalHistory.length - 1]; goal.currentStep++; // Push the current step into the history state and load the data. await dispatch(asyncRefreshWords()); @@ -138,13 +126,13 @@ export function asyncAdvanceStep() { export function asyncRefreshWords() { return async (dispatch: StoreStateDispatch, getState: () => StoreState) => { - let historyState = getState().goalsState.historyState; - let goal = historyState.history[historyState.history.length - 1]; + let goalHistory = getState().goalsState.historyState.history; + let goal = goalHistory[goalHistory.length - 1]; // Push the current step into the history state and load the data. - await updateStep(dispatch, goal, historyState).then(() => { - historyState = getState().goalsState.historyState; - goal = historyState.history[historyState.history.length - 1]; + await updateStep(dispatch, goal, goalHistory).then(() => { + goalHistory = getState().goalsState.historyState.history; + goal = goalHistory[goalHistory.length - 1]; if (goal.currentStep < goal.numSteps) { if (goal.goalType === GoalType.MergeDups) { getMergeStepData(goal, dispatch); @@ -173,83 +161,36 @@ export function updateStepData(goal: Goal): Goal { return goal; } -export function getUserEditId(user: User): string | undefined { - const projectId = LocalStorage.getProjectId(); - let projectIds = Object.keys(user.workedProjects); - let matches: string[] = projectIds.filter((project) => projectId === project); - if (matches.length === 1) { - return user.workedProjects[matches[0]]; - } -} - -export function getIndexInHistory(history: Goal[], currentGoal: Goal): number { - for (let i = 0; i < history.length; i++) { - if (history[i].hash === currentGoal.hash) { - return i; +export function getUserEditId(): string | undefined { + const user = LocalStorage.getCurrentUser(); + if (user) { + const projectId = LocalStorage.getProjectId(); + const projectIds = Object.keys(user.workedProjects); + const key = projectIds.find((id) => id === projectId); + if (key) { + return user.workedProjects[key]; } } - return -1; } -function convertEditsToArrayOfGoals(edits: Edit[]) { - const history: Goal[] = []; - for (const edit of edits) { - const nextGoal = goalTypeToGoal(edit.goalType); - history.push(nextGoal); - } - return history; +function convertEditsToGoals(edits: Edit[]): Goal[] { + return edits.map((edit) => goalTypeToGoal(edit.goalType)); } -function updateStep( +async function updateStep( dispatch: StoreStateDispatch, goal: Goal, - state: GoalHistoryState + goalHistory: Goal[] ): Promise { - return new Promise((resolve) => { - const updatedGoal = updateStepData(goal); - dispatch(updateGoal(updatedGoal)); - const goalIndex = getIndexInHistory(state.history, goal); - addStepToGoal(state.history[goalIndex], goalIndex); - resolve(); - }); + const goalIndex = goalHistory.findIndex((g) => g.hash === goal.hash); + const updatedGoal = updateStepData(goal); + dispatch(updateGoal(updatedGoal)); + await addStepToGoal(goalHistory[goalIndex], goalIndex); } async function addStepToGoal(goal: Goal, goalIndex: number) { - const user = LocalStorage.getCurrentUser(); - if (user) { - const userEditId: string | undefined = getUserEditId(user); - if (userEditId !== undefined) { - await Backend.addStepToGoal(userEditId, goalIndex, goal); - } - } -} - -export async function saveChanges( - goal: Goal, - history: Goal[], - project: Project, - dispatch: StoreStateDispatch -) { - await saveChangesToGoal(goal, history, dispatch); - await saveChangesToProject(project, dispatch); -} - -async function saveChangesToGoal( - updatedGoal: Goal, - history: Goal[], - dispatch: StoreStateDispatch -) { - const user = LocalStorage.getCurrentUser(); - if (user) { - const userEditId = getUserEditId(user); - if (userEditId !== undefined) { - const goalIndex = getIndexInHistory(history, updatedGoal); - dispatch(updateGoal(updatedGoal)); - await Backend.addStepToGoal( - userEditId, - goalIndex, - updatedGoal - ).catch((err) => console.error(err)); - } + const userEditId = getUserEditId(); + if (userEditId) { + await Backend.addStepToGoal(userEditId, goalIndex, goal); } } diff --git a/src/components/GoalTimeline/index.tsx b/src/components/GoalTimeline/index.tsx index 90b990de54..7eafaf1e99 100644 --- a/src/components/GoalTimeline/index.tsx +++ b/src/components/GoalTimeline/index.tsx @@ -9,7 +9,7 @@ import { asyncAddGoalToHistory, asyncGetUserEdits } from "./GoalsActions"; export function mapStateToProps(state: StoreState) { return { allPossibleGoals: state.goalsState.allPossibleGoals, - history: state.goalsState.historyState.history.reverse(), + history: state.goalsState.historyState.history, suggestions: state.goalsState.suggestionsState.suggestions, }; } diff --git a/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx b/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx index 49bf09ec3a..f0a94a56ae 100644 --- a/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx +++ b/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx @@ -14,7 +14,6 @@ import { MergeTreeActions, } from "../../../goals/MergeDupGoal/MergeDupStep/MergeDupStepActions"; import { goalDataMock } from "../../../goals/MergeDupGoal/MergeDupStep/tests/MockMergeDupData"; -import { ReviewEntries } from "../../../goals/ReviewEntries/ReviewEntries"; import { Goal } from "../../../types/goals"; import { maxNumSteps } from "../../../types/goalUtilities"; import { User } from "../../../types/user"; @@ -66,11 +65,11 @@ let mockStore: MockStoreEnhanced; let oldProjectId: string; let oldUser: User | null; -const mockProjectId: string = "12345"; -const mockUserEditId: string = "23456"; +const mockProjectId = "123"; +const mockUserEditId = "456"; const mockUserEdit: UserEdit = { id: mockUserEditId, edits: [] }; -const mockUserId: string = "34567"; -let mockUser: User = new User("", "", ""); +const mockUserId = "789"; +let mockUser = new User("", "", ""); mockUser.id = mockUserId; mockUser.workedProjects[mockProjectId] = mockUserEditId; const mockGoal: Goal = new CreateCharInv(); @@ -100,7 +99,7 @@ beforeAll(() => { beforeEach(() => { // Clear everything from localStorage interacted with by these tests. LocalStorage.remove(LocalStorage.LocalStorageKey.ProjectId); - LocalStorage.remove(LocalStorage.LocalStorageKey.User); + LocalStorage.setCurrentUser(mockUser); }); afterEach(() => { @@ -114,8 +113,8 @@ afterAll(() => { } }); -describe("Test GoalsActions", () => { - it("should create an action to add a goal to history", () => { +describe("GoalsActions", () => { + it("AddGoalToHistoryAction should create an action to add a goal to history", () => { const goal: Goal = new CreateCharInv(); const expectedAction: actions.AddGoalToHistoryAction = { type: actions.GoalsActions.ADD_GOAL_TO_HISTORY, @@ -124,7 +123,7 @@ describe("Test GoalsActions", () => { expect(actions.addGoalToHistory(goal)).toEqual(expectedAction); }); - it("should create an action to load user edits", () => { + it("LoadUserEditsAction should create an action to load user edits", () => { const goalHistory: Goal[] = [new CreateCharInv(), new MergeDups()]; const expectedAction: actions.LoadUserEditsAction = { type: actions.GoalsActions.LOAD_USER_EDITS, @@ -133,7 +132,7 @@ describe("Test GoalsActions", () => { expect(actions.loadUserEdits(goalHistory)).toEqual(expectedAction); }); - it("should create an action to update a goal", () => { + it("UpdateGoalAction should create an action to update a goal", () => { const goal: Goal = new CreateCharInv(); const expectedAction: actions.UpdateGoalAction = { type: actions.GoalsActions.UPDATE_GOAL, @@ -142,7 +141,7 @@ describe("Test GoalsActions", () => { expect(actions.updateGoal(goal)).toEqual(expectedAction); }); - it("should create an async action to load user edits", async () => { + it("asyncLoadExistingUserEdits should create an async action to load user edits", async () => { await mockStore.dispatch( actions.asyncLoadExistingUserEdits(mockProjectId, mockUserEditId) ); @@ -154,205 +153,189 @@ describe("Test GoalsActions", () => { expect(mockStore.getActions()).toEqual([loadUserEdits]); }); - it("should dispatch an action to load a user edit", async () => { - LocalStorage.setCurrentUser(mockUser); - LocalStorage.setProjectId(mockProjectId); - - await mockStore - .dispatch(actions.asyncGetUserEdits()) - .then(() => {}) - .catch((err: string) => { - fail(err); - }); - - let loadUserEditsAction: actions.LoadUserEditsAction = { - type: actions.GoalsActions.LOAD_USER_EDITS, - payload: [], - }; - - expect(mockStore.getActions()).toEqual([loadUserEditsAction]); + describe("asyncGetUserEdits", () => { + it("should dispatch an action to load a user edit", async () => { + LocalStorage.setCurrentUser(mockUser); + LocalStorage.setProjectId(mockProjectId); + + await mockStore + .dispatch(actions.asyncGetUserEdits()) + .then(() => {}) + .catch((err: string) => { + fail(err); + }); + + let loadUserEditsAction: actions.LoadUserEditsAction = { + type: actions.GoalsActions.LOAD_USER_EDITS, + payload: [], + }; + + expect(mockStore.getActions()).toEqual([loadUserEditsAction]); + }); + + it("should not dispatch any actions when creating a new user edit", async () => { + LocalStorage.setCurrentUser(mockUser); + + await mockStore + .dispatch(actions.asyncGetUserEdits()) + .then(() => {}) + .catch((err: string) => { + fail(err); + }); + + expect(mockStore.getActions()).toEqual([]); + }); }); - it("should not dispatch any actions when creating a new user edit", async () => { - LocalStorage.setCurrentUser(mockUser); - - await mockStore - .dispatch(actions.asyncGetUserEdits()) - .then(() => {}) - .catch((err: string) => { - fail(err); - }); - - expect(mockStore.getActions()).toEqual([]); - }); - - it("should create an async action to add a goal to history", async () => { - const goal: Goal = new CreateCharInv(); - LocalStorage.setCurrentUser(mockUser); - LocalStorage.setProjectId(mockProjectId); + describe("asyncAddGoalToHistory", () => { + it("should create an async action to add a goal to history", async () => { + const goal: Goal = new CreateCharInv(); + LocalStorage.setCurrentUser(mockUser); + LocalStorage.setProjectId(mockProjectId); - await mockStore.dispatch(actions.asyncAddGoalToHistory(goal)); + await mockStore.dispatch(actions.asyncAddGoalToHistory(goal)); - let addGoalToHistory: actions.AddGoalToHistoryAction = { - type: actions.GoalsActions.ADD_GOAL_TO_HISTORY, - payload: [goal], - }; + let addGoalToHistory: actions.AddGoalToHistoryAction = { + type: actions.GoalsActions.ADD_GOAL_TO_HISTORY, + payload: [goal], + }; - expect(mockStore.getActions()).toEqual([addGoalToHistory]); + expect(mockStore.getActions()).toEqual([addGoalToHistory]); + }); }); - it("should dispatch UPDATE_GOAL and SET_DATA", async () => { - let goalToUpdate: Goal = new MergeDups(); - goalToUpdate.numSteps = maxNumSteps(goalToUpdate.goalType); - goalToUpdate.steps = [ - { - words: [...goalDataMock.plannedWords[0]], - }, - ]; - - let expectedUpdatedGoal: Goal = new MergeDups(); - expectedUpdatedGoal.currentStep = 0; - expectedUpdatedGoal.hash = goalToUpdate.hash; - expectedUpdatedGoal.numSteps = goalToUpdate.numSteps; - expectedUpdatedGoal.data = { - plannedWords: [...goalDataMock.plannedWords], - }; - expectedUpdatedGoal.steps = [ - { - words: [...goalDataMock.plannedWords[0]], - }, - ]; - - let updateGoal: actions.UpdateGoalAction = { - type: actions.GoalsActions.UPDATE_GOAL, - payload: [expectedUpdatedGoal], - }; - - let setWordData: MergeTreeAction = { - type: MergeTreeActions.SET_DATA, - payload: [...goalDataMock.plannedWords[0]], - }; - - const mockStoreState = { - goalsState: { - historyState: { - history: [goalToUpdate], + describe("asyncLoadGoalData", () => { + it("should dispatch UPDATE_GOAL and SET_DATA", async () => { + let goalToUpdate: Goal = new MergeDups(); + goalToUpdate.numSteps = maxNumSteps(goalToUpdate.goalType); + goalToUpdate.steps = [ + { + words: [...goalDataMock.plannedWords[0]], }, - allPossibleGoals: [...goalsDefaultState.allPossibleGoals], - suggestionsState: { - suggestions: [...goalsDefaultState.suggestionsState.suggestions], + ]; + + let expectedUpdatedGoal: Goal = new MergeDups(); + expectedUpdatedGoal.currentStep = 0; + expectedUpdatedGoal.hash = goalToUpdate.hash; + expectedUpdatedGoal.numSteps = goalToUpdate.numSteps; + expectedUpdatedGoal.data = { + plannedWords: [...goalDataMock.plannedWords], + }; + expectedUpdatedGoal.steps = [ + { + words: [...goalDataMock.plannedWords[0]], }, - }, - }; - - mockStore = createMockStore(mockStoreState); - - try { - await mockStore.dispatch(actions.asyncLoadGoalData(goalToUpdate)); - } catch (err) { - fail(err); - } - expect(mockStore.getActions()).toEqual([updateGoal, setWordData]); - }); - - it("should not dispatch any actions", async () => { - const goal: Goal = new HandleFlags(); - const expectedGoal: Goal = new HandleFlags(); - - await mockStore - .dispatch(actions.asyncLoadGoalData(goal)) - .then((returnedGoal: Goal) => { - expect(returnedGoal.data).toEqual(expectedGoal.data); - }) - .catch((err: string) => fail(err)); - - expect(mockStore.getActions()).toEqual([]); - }); - - it("should load goal data for MergeDups", async () => { - let goal: Goal = new MergeDups(); - try { - goal = await mockStore.dispatch(actions.asyncLoadGoalData(goal)); - let data = goal.data as MergeDupData; - expect(data.plannedWords.length).toBeGreaterThan(0); - } catch (err) { - fail(err); - } - }); - - it("should not load any goal data", async () => { - const goal: Goal = new HandleFlags(); - - await mockStore - .dispatch(actions.asyncLoadGoalData(goal)) - .then((returnedGoal: Goal) => { - expect(returnedGoal.data).toEqual({}); - }) - .catch((err: string) => fail(err)); - }); - - it("Should update the step data of a goal", () => { - const goal = new MergeDups(); - goal.data = goalDataMock; - expect(goal.steps).toEqual([]); - expect(goal.currentStep).toEqual(0); - - const updatedGoal = actions.updateStepData(goal); - - expect((updatedGoal.steps[0] as MergeStepData).words).toEqual( - (goal.data as MergeDupData).plannedWords[0] - ); - expect(updatedGoal.currentStep).toEqual(0); - }); - - it("Should not update the step data of an unimplemented goal", () => { - const goal: HandleFlags = new HandleFlags(); - expect(goal.steps).toEqual([]); - expect(goal.currentStep).toEqual(0); + ]; + + let updateGoal: actions.UpdateGoalAction = { + type: actions.GoalsActions.UPDATE_GOAL, + payload: [expectedUpdatedGoal], + }; + + let setWordData: MergeTreeAction = { + type: MergeTreeActions.SET_DATA, + payload: [...goalDataMock.plannedWords[0]], + }; + + const mockStoreState = { + goalsState: { + historyState: { + history: [goalToUpdate], + }, + allPossibleGoals: [...goalsDefaultState.allPossibleGoals], + suggestionsState: { + suggestions: [...goalsDefaultState.suggestionsState.suggestions], + }, + }, + }; - const updatedGoal: HandleFlags = actions.updateStepData( - goal - ) as HandleFlags; + mockStore = createMockStore(mockStoreState); - expect(updatedGoal.steps).toEqual([]); - expect(updatedGoal.currentStep).toEqual(0); + try { + await mockStore.dispatch(actions.asyncLoadGoalData(goalToUpdate)); + } catch (err) { + fail(err); + } + expect(mockStore.getActions()).toEqual([updateGoal, setWordData]); + }); + + it("should not dispatch any actions", async () => { + const goal: Goal = new HandleFlags(); + const expectedGoal: Goal = new HandleFlags(); + + await mockStore + .dispatch(actions.asyncLoadGoalData(goal)) + .then((returnedGoal: Goal) => { + expect(returnedGoal.data).toEqual(expectedGoal.data); + }) + .catch((err: string) => fail(err)); + + expect(mockStore.getActions()).toEqual([]); + }); + + it("should load goal data for MergeDups", async () => { + let goal: Goal = new MergeDups(); + try { + goal = await mockStore.dispatch(actions.asyncLoadGoalData(goal)); + let data = goal.data as MergeDupData; + expect(data.plannedWords.length).toBeGreaterThan(0); + } catch (err) { + fail(err); + } + }); + + it("should not load any goal data", async () => { + const goal: Goal = new HandleFlags(); + + await mockStore + .dispatch(actions.asyncLoadGoalData(goal)) + .then((returnedGoal: Goal) => { + expect(returnedGoal.data).toEqual({}); + }) + .catch((err: string) => fail(err)); + }); }); - it("should return a userEditId", () => { - LocalStorage.setProjectId(mockProjectId); - expect(actions.getUserEditId(mockUser)).toEqual(mockUserEditId); - }); + describe("updateStepData", () => { + it("should update the step data of a goal", () => { + const goal = new MergeDups(); + goal.data = goalDataMock; + expect(goal.steps).toEqual([]); + expect(goal.currentStep).toEqual(0); - it("should return undefined when no projectId is set", () => { - expect(actions.getUserEditId(mockUser)).toEqual(undefined); - }); + const updatedGoal = actions.updateStepData(goal); - it("should return undefined when no userId exists for the project", () => { - LocalStorage.setProjectId("differentThanMockProjectId"); - expect(actions.getUserEditId(mockUser)).toEqual(undefined); - }); + expect((updatedGoal.steps[0] as MergeStepData).words).toEqual( + (goal.data as MergeDupData).plannedWords[0] + ); + expect(updatedGoal.currentStep).toEqual(0); + }); - it("should return the correct goal", () => { - const goal: Goal = new HandleFlags(); - const goal2: Goal = new CreateCharInv(); - const goal3: Goal = new MergeDups(); - const history: Goal[] = [goal, goal2, goal3]; + it("should not update the step data of an unimplemented goal", () => { + const goal = new HandleFlags(); + expect(goal.steps).toEqual([]); + expect(goal.currentStep).toEqual(0); - const currentGoal: Goal = goal2; - let returnedIndex = actions.getIndexInHistory(history, currentGoal); + const updatedGoal: HandleFlags = actions.updateStepData(goal); - expect(returnedIndex).toEqual(1); + expect(updatedGoal.steps).toEqual([]); + expect(updatedGoal.currentStep).toEqual(0); + }); }); - it("should return -1 when a goal doesn't exist", () => { - const goal: Goal = new HandleFlags(); - const goal2: Goal = new CreateCharInv(); - const goal3: Goal = new MergeDups(); - const history: Goal[] = [goal, goal2, goal3]; + describe("getUserEditId", () => { + it("should return a userEditId", () => { + LocalStorage.setProjectId(mockProjectId); + expect(actions.getUserEditId()).toEqual(mockUserEditId); + }); - const currentGoal: Goal = new ReviewEntries(); - let returnedIndex = actions.getIndexInHistory(history, currentGoal); + it("should return undefined when no projectId is set", () => { + expect(actions.getUserEditId()).toEqual(undefined); + }); - expect(returnedIndex).toEqual(-1); + it("should return undefined when no userEditId exists for the project", () => { + LocalStorage.setProjectId("differentThanMockProjectId"); + expect(actions.getUserEditId()).toEqual(undefined); + }); }); }); diff --git a/src/components/GoalTimeline/tests/GoalTimelineComponent.test.tsx b/src/components/GoalTimeline/tests/GoalTimelineComponent.test.tsx index d694964d39..9eb335479c 100644 --- a/src/components/GoalTimeline/tests/GoalTimelineComponent.test.tsx +++ b/src/components/GoalTimeline/tests/GoalTimelineComponent.test.tsx @@ -40,18 +40,6 @@ beforeEach(() => { }); describe("GoalTimelineVertical", () => { - describe("findGoalByName", () => { - it("Finds a goal by name when prompted", () => { - expect(timeHandle.findGoalByName(goals, goals[2].name)).toEqual(goals[2]); - }); - - it("Returns undefined when prompted for a non-existant goal", () => { - expect(timeHandle.findGoalByName(goals.slice(1), goals[0].name)).toBe( - undefined - ); - }); - }); - describe("handleChange", () => { it("Selects a goal from suggestions based on name", () => { timeHandle.handleChange(goals[2].name); diff --git a/src/components/ProjectScreen/CreateProject/CreateProjectActions.tsx b/src/components/ProjectScreen/CreateProject/CreateProjectActions.tsx index 3017528c9f..8e8b453173 100644 --- a/src/components/ProjectScreen/CreateProject/CreateProjectActions.tsx +++ b/src/components/ProjectScreen/CreateProject/CreateProjectActions.tsx @@ -2,7 +2,7 @@ import * as backend from "../../../backend"; import history, { Path } from "../../../history"; import { StoreStateDispatch } from "../../../types/actions"; import { defaultProject, Project, WritingSystem } from "../../../types/project"; -import { asyncGetUserEdits } from "../../GoalTimeline/GoalsActions"; +import { asyncCreateUserEdits } from "../../GoalTimeline/GoalsActions"; import { setCurrentProject } from "../../Project/ProjectActions"; export const IN_PROGRESS = "CREATE_PROJECT_IN_PROGRESS"; @@ -63,7 +63,7 @@ export function asyncCreateProject( dispatch(success(name, vernacularLanguage, analysisLanguages)); // we manually pause so they have a chance to see the success message setTimeout(() => { - dispatch(asyncGetUserEdits()); + dispatch(asyncCreateUserEdits()); history.push(Path.ProjSettings); }, 1000); }) @@ -81,7 +81,7 @@ export function asyncCreateProject( } else { dispatch(success(name, vernacularLanguage, analysisLanguages)); setTimeout(() => { - dispatch(asyncGetUserEdits()); + dispatch(asyncCreateUserEdits()); history.push(Path.ProjSettings); }, 1000); } diff --git a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx index d0b03e84ce..ef0557728b 100644 --- a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx +++ b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx @@ -1,10 +1,8 @@ import * as backend from "../../backend"; -import { saveChanges } from "../../components/GoalTimeline/GoalsActions"; +import { saveChangesToProject } from "../../components/Project/ProjectActions"; import { StoreState } from "../../types"; import { StoreStateDispatch } from "../../types/actions"; -import { Goal } from "../../types/goals"; import { Project } from "../../types/project"; -import { CreateCharInv } from "../CreateCharInv/CreateCharInv"; import { CharacterSetEntry, characterStatus, @@ -123,11 +121,8 @@ export function setCharacterStatus(character: string, status: characterStatus) { export function uploadInventory() { return async (dispatch: StoreStateDispatch, getState: () => StoreState) => { const state = getState(); - const updatedGoal = updateCurrentGoal(state); - const goalHistory = state.goalsState.historyState.history; const updatedProject = updateCurrentProject(state); - - await saveChanges(updatedGoal, goalHistory, updatedProject, dispatch); + await saveChangesToProject(updatedProject, dispatch); }; } @@ -195,11 +190,3 @@ function updateCurrentProject(state: StoreState): Project { project.rejectedCharacters = state.characterInventoryState.rejectedCharacters; return project; } - -function updateCurrentGoal(state: StoreState): Goal { - const history = state.goalsState.historyState.history; - const currentGoal = history[history.length - 1] as CreateCharInv; - // Nothing stored as goal data for now - - return currentGoal; -} diff --git a/src/goals/CharInventoryCreation/tests/CharacterInventoryActions.test.tsx b/src/goals/CharInventoryCreation/tests/CharacterInventoryActions.test.tsx index b0b238d221..0a51afbc62 100644 --- a/src/goals/CharInventoryCreation/tests/CharacterInventoryActions.test.tsx +++ b/src/goals/CharInventoryCreation/tests/CharacterInventoryActions.test.tsx @@ -3,13 +3,10 @@ import thunk from "redux-thunk"; import * as backend from "../../../backend"; import * as LocalStorage from "../../../backend/localStorage"; -import { GoalsActions } from "../../../components/GoalTimeline/GoalsActions"; import { SET_CURRENT_PROJECT } from "../../../components/Project/ProjectActions"; import { StoreState } from "../../../types"; -import { Goal } from "../../../types/goals"; import { Project } from "../../../types/project"; import { User } from "../../../types/user"; -import { CreateCharInv } from "../../CreateCharInv/CreateCharInv"; import { CharacterInventoryType, setValidCharacters, @@ -46,13 +43,7 @@ const CHARACTER_SET_DATA: CharacterSetEntry[] = [ occurrences: 0, }, ]; -const goal: CreateCharInv = new CreateCharInv(); const MOCK_STATE = { - goalsState: { - historyState: { - history: [goal], - }, - }, currentProject: { characterSet: null, }, @@ -65,10 +56,10 @@ const MOCK_STATE = { let oldProjectId: string; let oldUser: User | null; -const mockProjectId: string = "12345"; -const mockUserEditId: string = "23456"; -const mockUserId: string = "34456"; -let mockUser: User = new User("", "", ""); +const mockProjectId = "123"; +const mockUserEditId = "456"; +const mockUserId = "789"; +let mockUser = new User("", "", ""); mockUser.id = mockUserId; mockUser.workedProjects[mockProjectId] = mockUserEditId; @@ -76,13 +67,8 @@ jest.mock("../../../backend", () => ({ updateProject: jest.fn((_project: Project) => { return Promise.resolve("projectId"); }), - addStepToGoal: jest.fn(() => { - return Promise.resolve(mockGoal); - }), })); -const mockGoal: Goal = new CreateCharInv(); - const createMockStore = configureMockStore([thunk]); const mockStore: MockStoreEnhanced = createMockStore(MOCK_STATE); @@ -125,18 +111,8 @@ describe("Testing CharacterInventoryActions", () => { mockStore.dispatch, mockStore.getState as () => StoreState ); - - const updatedGoal: CreateCharInv = goal; - updatedGoal.data = { - inventory: [[...MOCK_STATE.characterInventoryState.validCharacters]], - }; expect(backend.updateProject).toHaveBeenCalledTimes(1); - expect(backend.addStepToGoal).toHaveBeenCalledTimes(1); expect(mockStore.getActions()).toEqual([ - { - type: GoalsActions.UPDATE_GOAL, - payload: [updatedGoal], - }, { type: SET_CURRENT_PROJECT, payload: { diff --git a/src/rootReducer.tsx b/src/rootReducer.tsx index 80385220d3..50d68229e9 100644 --- a/src/rootReducer.tsx +++ b/src/rootReducer.tsx @@ -2,7 +2,6 @@ import { localizeReducer } from "react-localize-redux"; import { combineReducers, Reducer } from "redux"; import { goalsReducer } from "./components/GoalTimeline/GoalsReducer"; -import { goalSelectReducer } from "./components/GoalTimeline/GoalSelectorScroll/GoalSelectorReducer"; import { loginReducer } from "./components/Login/LoginReducer"; import { passwordResetReducer } from "./components/PasswordReset/reducer"; import { projectReducer } from "./components/Project/ProjectReducer"; @@ -35,7 +34,6 @@ export const rootReducer: Reducer = combineReducers({ pronunciationsState: pronunciationsReducer, //general cleanup tools - goalSelectorState: goalSelectReducer, goalsState: goalsReducer, //merge duplicates goal diff --git a/src/types/goals.tsx b/src/types/goals.tsx index c47b3f4a6a..52ea4860d3 100644 --- a/src/types/goals.tsx +++ b/src/types/goals.tsx @@ -38,17 +38,6 @@ export interface GoalSuggestionsState { suggestions: Goal[]; } -export interface GoalSwitcherState { - goals: Goal[]; -} - -export interface GoalSelectorState { - selectedIndex: number; - allPossibleGoals: Goal[]; - mouseX: number; - lastIndex: number; -} - // The enum value is a permanent id for UserEdits and should not be changed. export enum GoalType { Default = -1, diff --git a/src/types/index.tsx b/src/types/index.tsx index 46a07157ff..d5983f7fe1 100644 --- a/src/types/index.tsx +++ b/src/types/index.tsx @@ -10,7 +10,7 @@ import { TreeViewState } from "../components/TreeView/TreeViewReducer"; import { CharacterInventoryState } from "../goals/CharInventoryCreation/CharacterInventoryReducer"; import { MergeTreeState } from "../goals/MergeDupGoal/MergeDupStep/MergeDupStepReducer"; import { ReviewEntriesState } from "../goals/ReviewEntries/ReviewEntriesComponent/ReviewEntriesReducer"; -import { GoalsState, GoalSelectorState } from "./goals"; +import { GoalsState } from "./goals"; import { Project } from "./project"; //root store structure @@ -33,7 +33,6 @@ export interface StoreState { pronunciationsState: PronunciationsState; //general cleanup tools - goalSelectorState: GoalSelectorState; goalsState: GoalsState; //merge duplicates goal