From b0ac4fadcc978d11332ea020758cb1e2c429ce8e Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 11 Feb 2021 10:42:50 -0500 Subject: [PATCH 01/11] Replace unused .result with .changes. --- src/types/goals.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/types/goals.tsx b/src/types/goals.tsx index 30b642eff8..c888aa0f32 100644 --- a/src/types/goals.tsx +++ b/src/types/goals.tsx @@ -7,12 +7,6 @@ import { import { MergeDupData, MergeStepData } from "goals/MergeDupGoal/MergeDups"; import { User } from "types/user"; -enum GoalOption { - Complete, - Abandon, - Current, -} - type GoalData = CreateCharInvData | MergeDupData | {}; // | OtherTypes export type GoalStep = CreateCharInvStepData | MergeStepData | {}; // | OtherTypes @@ -71,7 +65,7 @@ export class Goal { currentStep: number; data: GoalData; completed: boolean; - result: GoalOption; + changes: any; hash: string; constructor( @@ -88,7 +82,7 @@ export class Goal { this.currentStep = 0; this.data = data; this.completed = false; - this.result = GoalOption.Current; + this.changes = {}; this.hash = v4(); } } From dbbc44e140634c08c10ad6dea257499b86ff8381 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 11 Feb 2021 11:12:44 -0500 Subject: [PATCH 02/11] Add Changes to backend Edit. --- Backend.Tests/Models/UserEditTests.cs | 15 ++++++++++++++- Backend/Models/UserEdit.cs | 12 +++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Backend.Tests/Models/UserEditTests.cs b/Backend.Tests/Models/UserEditTests.cs index 6c5e347cf8..c9d2a00e5d 100644 --- a/Backend.Tests/Models/UserEditTests.cs +++ b/Backend.Tests/Models/UserEditTests.cs @@ -1,4 +1,5 @@ -using BackendFramework.Models; +using System.Collections.Generic; +using BackendFramework.Models; using NUnit.Framework; namespace Backend.Tests.Models @@ -26,6 +27,8 @@ public void TestEqualsNull() public class EditTests { private const int GoalType = 1; + private List StepData = new List() { "step" }; + private const string Changes = "{wordIds:[]}"; [Test] public void TestEquals() @@ -33,6 +36,12 @@ public void TestEquals() var edit = new Edit { GoalType = GoalType }; Assert.That(edit.Equals(new Edit { GoalType = GoalType })); + edit.StepData = StepData; + Assert.That(edit.Equals(new Edit { GoalType = GoalType, StepData = StepData })); + edit.Changes = Changes; + Assert.That(edit.Equals( + new Edit { GoalType = GoalType, StepData = StepData, Changes = Changes })); + } [Test] @@ -40,6 +49,10 @@ public void TestEqualsNull() { var edit = new Edit { GoalType = GoalType }; Assert.IsFalse(edit.Equals(null)); + edit = new Edit { StepData = StepData }; + Assert.IsFalse(edit.Equals(null)); + edit = new Edit { Changes = Changes }; + Assert.IsFalse(edit.Equals(null)); } } } diff --git a/Backend/Models/UserEdit.cs b/Backend/Models/UserEdit.cs index 94b1017321..6d8116d04d 100644 --- a/Backend/Models/UserEdit.cs +++ b/Backend/Models/UserEdit.cs @@ -113,10 +113,14 @@ public class Edit [BsonElement("stepData")] public List StepData { get; set; } + [BsonElement("changes")] + public string Changes { get; set; } + public Edit() { GoalType = 0; StepData = new List(); + Changes = "{}"; } public Edit Clone() @@ -124,7 +128,8 @@ public Edit Clone() var clone = new Edit { GoalType = GoalType, - StepData = new List() + StepData = new List(), + Changes = (string)Changes.Clone() }; foreach (var stepData in StepData) @@ -145,12 +150,13 @@ public override bool Equals(object? obj) return GoalType.Equals(other.GoalType) && other.StepData.Count == StepData.Count && - other.StepData.All(StepData.Contains); + other.StepData.All(StepData.Contains) && + other.Changes.Equals(Changes); } public override int GetHashCode() { - return HashCode.Combine(GoalType, StepData); + return HashCode.Combine(GoalType, StepData, Changes); } } From ed8e68cc256fc0c721c7268ce015f09e2a5b0a52 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 11 Feb 2021 11:59:59 -0500 Subject: [PATCH 03/11] Cleanup goal-edit, backend-frontend types and interactions. --- Backend/Controllers/UserEditController.cs | 2 + src/backend/index.tsx | 71 ++++++------------- src/components/GoalTimeline/GoalsActions.tsx | 11 +-- .../tests/GoalTimelineActions.test.tsx | 27 +------ src/types/goalUtilities.tsx | 17 +++++ src/types/tests/goalUtilities.test.tsx | 33 +++++++++ src/types/userEdit.ts | 1 + 7 files changed, 77 insertions(+), 85 deletions(-) create mode 100644 src/types/tests/goalUtilities.test.tsx diff --git a/Backend/Controllers/UserEditController.cs b/Backend/Controllers/UserEditController.cs index 4d14e0e56c..eadd47242c 100644 --- a/Backend/Controllers/UserEditController.cs +++ b/Backend/Controllers/UserEditController.cs @@ -32,6 +32,7 @@ public UserEditController(IUserEditRepository repo, IUserEditService userEditSer /// Returns all s for specified /// GET: v1/projects/{projectId}/useredits + /// UserEdit list [HttpGet] public async Task Get(string projectId) { @@ -80,6 +81,7 @@ public async Task Delete(string projectId) /// Returns s with specified id /// GET: v1/projects/{projectId}/useredits/{userEditId} + /// UserEdit [HttpGet("{userEditId}")] public async Task Get(string projectId, string userEditId) { diff --git a/src/backend/index.tsx b/src/backend/index.tsx index 7dc574f78f..6a4518de38 100644 --- a/src/backend/index.tsx +++ b/src/backend/index.tsx @@ -1,8 +1,10 @@ import axios from "axios"; -import authHeader from "components/Login/AuthHeaders"; +import * as LocalStorage from "backend/localStorage"; import history, { Path } from "browserHistory"; +import authHeader from "components/Login/AuthHeaders"; import { Goal, GoalStep } from "types/goals"; +import { convertGoalToEdit } from "types/goalUtilities"; import { Project } from "types/project"; import { RuntimeConfig } from "types/runtimeConfig"; import SemanticDomainWithSubdomains from "types/SemanticDomain"; @@ -10,7 +12,6 @@ import { User } from "types/user"; import { UserEdit } from "types/userEdit"; import { UserRole } from "types/userRole"; import { MergeWord, Word } from "types/word"; -import * as LocalStorage from "backend/localStorage"; export const baseURL = `${RuntimeConfig.getInstance().baseUrl()}`; const apiBaseURL = `${baseURL}/v1`; @@ -72,9 +73,7 @@ export async function createWord(word: Word): Promise { export async function getWord(id: string): Promise { let resp = await backendServer.get( `projects/${LocalStorage.getProjectId()}/words/${id}`, - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -82,9 +81,7 @@ export async function getWord(id: string): Promise { export async function getAllWords(): Promise { let resp = await backendServer.get( `projects/${LocalStorage.getProjectId()}/words`, - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -192,9 +189,7 @@ export async function getAllUsers(): Promise { export async function getAllUsersInCurrentProject(): Promise { let resp = await backendServer.get( `projects/${LocalStorage.getProjectId()}/users`, - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -400,27 +395,17 @@ export async function avatarSrc(userId: string): Promise { return `data:${resp.headers["content-type"].toLowerCase()};base64,${image}`; } -function convertGoalToBackendEdit(goal: Goal) { - const stepData = goal.steps.map((s) => JSON.stringify(s)); - return { - goalType: goal.goalType.toString(), - stepData, - }; -} - /** Returns index of added goal */ export async function addGoalToUserEdit( userEditId: string, goal: Goal ): Promise { - const userEditTuple = convertGoalToBackendEdit(goal); + const edit = convertGoalToEdit(goal); const projectId = LocalStorage.getProjectId(); const resp = await backendServer.post( `projects/${projectId}/useredits/${userEditId}`, - userEditTuple, - { - headers: authHeader(), - } + edit, + { headers: authHeader() } ); return resp.data; } @@ -438,22 +423,19 @@ export async function addStepToGoal( .put( `projects/${LocalStorage.getProjectId()}/useredits/${userEditId}`, stepEditTuple, - { - headers: { ...authHeader() }, - } + { headers: authHeader() } ) .then((resp) => { return resp.data; }); } -export async function createUserEdit(): Promise { +/** Returns User with updated .workedProjects */ +export async function createUserEdit(): Promise { let resp = await backendServer.post( `projects/${LocalStorage.getProjectId()}/useredits`, "", - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -468,12 +450,11 @@ export async function getUserEditById( return resp.data; } -export async function getAllUserEdits(): Promise { +/** Returns array with every UserEdit for the current project */ +export async function getAllUserEdits(): Promise { let resp = await backendServer.get( `projects/${LocalStorage.getProjectId()}/useredits`, - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -491,9 +472,7 @@ export async function getSemanticDomains(): Promise< export async function getUserRoles(): Promise { let resp = await backendServer.get( `projects/${LocalStorage.getProjectId()}/userroles`, - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -501,9 +480,7 @@ export async function getUserRoles(): Promise { export async function canUploadLift(): Promise { let resp = await backendServer.get( `projects/${LocalStorage.getProjectId()}/liftcheck`, - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -515,9 +492,7 @@ export async function addUserRole( await backendServer.put( `projects/${LocalStorage.getProjectId()}/users/${user.id}`, permissions, - { - headers: authHeader(), - } + { headers: authHeader() } ); } @@ -534,9 +509,7 @@ export async function emailInviteToProject( ProjectId: projectId, Domain: window.location.origin, }, - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } @@ -548,9 +521,7 @@ export async function validateLink( let resp = await backendServer.put( `projects/invite/${projectId}/validate/${token}`, "", - { - headers: authHeader(), - } + { headers: authHeader() } ); return resp.data; } diff --git a/src/components/GoalTimeline/GoalsActions.tsx b/src/components/GoalTimeline/GoalsActions.tsx index 4996c2c9de..49218b8643 100644 --- a/src/components/GoalTimeline/GoalsActions.tsx +++ b/src/components/GoalTimeline/GoalsActions.tsx @@ -9,8 +9,7 @@ import history, { Path } from "browserHistory"; import { StoreState } from "types"; import { ActionWithPayload, StoreStateDispatch } from "types/actions"; import { Goal, GoalType } from "types/goals"; -import { goalTypeToGoal } from "types/goalUtilities"; -import { Edit } from "types/userEdit"; +import { convertEditToGoal } from "types/goalUtilities"; export enum GoalsActions { LOAD_USER_EDITS = "LOAD_USER_EDITS", @@ -185,14 +184,6 @@ export function getUserEditId(): string | undefined { } } -export function convertEditToGoal(edit: Edit): Goal { - const goal = goalTypeToGoal(edit.goalType); - goal.steps = edit.stepData.map((stepString) => JSON.parse(stepString)); - goal.numSteps = goal.steps.length; - goal.completed = true; - return goal; -} - async function saveCurrentStep(goal: Goal, goalIndex: number) { const userEditId = getUserEditId(); if (userEditId) { diff --git a/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx b/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx index e352291af7..40a55373da 100644 --- a/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx +++ b/src/components/GoalTimeline/tests/GoalTimelineActions.test.tsx @@ -16,7 +16,7 @@ import { goalDataMock } from "goals/MergeDupGoal/MergeDupStep/tests/MockMergeDup import { Goal } from "types/goals"; import { maxNumSteps } from "types/goalUtilities"; import { User } from "types/user"; -import { Edit, UserEdit } from "types/userEdit"; +import { UserEdit } from "types/userEdit"; jest.mock("goals/MergeDupGoal/MergeDupStep/MergeDupStepActions", () => { const realMergeDupActions = jest.requireActual( @@ -58,7 +58,7 @@ const mockUpdateUser = jest.fn(); function setMockFunctions() { mockAddGoalToUserEdit.mockResolvedValue(0); mockAddStepToGoal.mockResolvedValue(0); - mockCreateUserEdit.mockResolvedValue({}); + mockCreateUserEdit.mockResolvedValue(mockUser); mockDispatchMergeStepData.mockReturnValue(mockAction); mockGetUser.mockResolvedValue(mockUser); mockGetUserEditById.mockResolvedValue(mockUserEdit); @@ -354,27 +354,4 @@ describe("GoalsActions", () => { expect(actions.getUserEditId()).toEqual(undefined); }); }); - - describe("convertEditToGoal", () => { - it("should build a completed goal with the same goalType and steps", () => { - const oldGoal: Goal = new MergeDups(); - oldGoal.numSteps = maxNumSteps(oldGoal.goalType); - oldGoal.steps = [ - { - words: [...goalDataMock.plannedWords[0]], - }, - { - words: [...goalDataMock.plannedWords[1]], - }, - ]; - const edit: Edit = { - goalType: oldGoal.goalType, - stepData: oldGoal.steps.map((s) => JSON.stringify(s)), - }; - const newGoal = actions.convertEditToGoal(edit); - expect(newGoal.goalType).toEqual(oldGoal.goalType); - expect(newGoal.steps).toEqual(oldGoal.steps); - expect(newGoal.numSteps).toEqual(oldGoal.steps.length); - }); - }); }); diff --git a/src/types/goalUtilities.tsx b/src/types/goalUtilities.tsx index 57d071d28a..1386e2bc10 100644 --- a/src/types/goalUtilities.tsx +++ b/src/types/goalUtilities.tsx @@ -7,6 +7,7 @@ import { SpellCheckGloss } from "goals/SpellCheckGloss/SpellCheckGloss"; import { ValidateChars } from "goals/ValidateChars/ValidateChars"; import { ValidateStrWords } from "goals/ValidateStrWords/ValidateStrWords"; import { Goal, GoalType } from "types/goals"; +import { Edit } from "types/userEdit"; export function maxNumSteps(type: GoalType) { switch (type) { @@ -39,3 +40,19 @@ export function goalTypeToGoal(type: GoalType) { return new Goal(); } } + +export function convertGoalToEdit(goal: Goal): Edit { + const goalType = goal.goalType as number; + const stepData = goal.steps.map((s) => JSON.stringify(s)); + const changes = JSON.stringify(goal.changes); + return { goalType, stepData, changes }; +} + +export function convertEditToGoal(edit: Edit): Goal { + const goal = goalTypeToGoal(edit.goalType); + goal.steps = edit.stepData.map((stepString) => JSON.parse(stepString)); + goal.numSteps = goal.steps.length; + goal.changes = JSON.parse(edit.changes); + goal.completed = true; + return goal; +} diff --git a/src/types/tests/goalUtilities.test.tsx b/src/types/tests/goalUtilities.test.tsx new file mode 100644 index 0000000000..c7a78f14f6 --- /dev/null +++ b/src/types/tests/goalUtilities.test.tsx @@ -0,0 +1,33 @@ +import { MergeDups } from "goals/MergeDupGoal/MergeDups"; +import { goalDataMock } from "goals/MergeDupGoal/MergeDupStep/tests/MockMergeDupData"; +import { Goal } from "types/goals"; +import { + convertEditToGoal, + convertGoalToEdit, + maxNumSteps, +} from "types/goalUtilities"; +import { Edit } from "types/userEdit"; + +describe("goalUtilities", () => { + describe("convertGoalToEdit, convertEditToGoal", () => { + it("should maintain goalType, steps, and changes", () => { + const oldGoal: Goal = new MergeDups(); + oldGoal.numSteps = maxNumSteps(oldGoal.goalType); + oldGoal.steps = [ + { + words: [...goalDataMock.plannedWords[0]], + }, + { + words: [...goalDataMock.plannedWords[1]], + }, + ]; + oldGoal.changes = { merges: [{}, {}] }; + const edit = convertGoalToEdit(oldGoal); + const newGoal = convertEditToGoal(edit); + expect(newGoal.goalType).toEqual(oldGoal.goalType); + expect(newGoal.steps).toEqual(oldGoal.steps); + expect(newGoal.numSteps).toEqual(oldGoal.steps.length); + expect(newGoal.changes).toEqual(oldGoal.changes); + }); + }); +}); diff --git a/src/types/userEdit.ts b/src/types/userEdit.ts index c2461651b6..a86587f1a1 100644 --- a/src/types/userEdit.ts +++ b/src/types/userEdit.ts @@ -6,4 +6,5 @@ export interface UserEdit { export interface Edit { goalType: number; stepData: string[]; + changes: string; } From fbadf4a0d431a47337a7be1b2a306f6504b4cbad Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 11 Feb 2021 16:20:10 -0500 Subject: [PATCH 04/11] CharInv refactor: implement function to compute changes, fix non-distinct keys error, fix sort menu icons, etc. --- .../CharacterInventoryActions.tsx | 91 ++++++++++++++++--- .../CharacterInventoryReducer.tsx | 8 +- .../CharacterDetailComponent.tsx | 4 +- ...terInfoComponent.tsx => CharacterInfo.tsx} | 9 +- .../CharacterDetail/CharacterInfo/index.tsx | 12 --- .../CharacterStatusControl/index.tsx | 7 +- .../CharacterDetail/CharacterWords.tsx | 72 +++++++++++++++ .../CharacterWordsComponent.tsx | 60 ------------ .../CharacterDetail/CharacterWords/index.tsx | 12 --- .../CharacterList/CharacterCard.tsx | 4 +- .../CharacterList/CharacterListComponent.tsx | 13 ++- .../CharacterList/CharacterStatusText.tsx | 8 +- .../tests/CharacterInventoryActions.test.tsx | 81 ++++++++++++----- .../CharacterInventoryComponent.test.tsx.snap | 11 ++- src/types/theme.ts | 2 +- 15 files changed, 255 insertions(+), 139 deletions(-) rename src/goals/CharInventoryCreation/components/CharacterDetail/{CharacterInfo/CharacterInfoComponent.tsx => CharacterInfo.tsx} (81%) delete mode 100644 src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/index.tsx create mode 100644 src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx delete mode 100644 src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/CharacterWordsComponent.tsx delete mode 100644 src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/index.tsx diff --git a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx index 18090f917b..0d633d715f 100644 --- a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx +++ b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx @@ -4,8 +4,9 @@ import { StoreState } from "types"; import { StoreStateDispatch } from "types/actions"; import { Project } from "types/project"; import { + CharacterInventoryState, CharacterSetEntry, - characterStatus, + CharacterStatus, } from "goals/CharInventoryCreation/CharacterInventoryReducer"; export enum CharacterInventoryType { @@ -96,12 +97,13 @@ export function resetInState(): CharacterInventoryAction { // Dispatch Functions -export function setCharacterStatus(character: string, status: characterStatus) { +export function setCharacterStatus(character: string, status: CharacterStatus) { return (dispatch: StoreStateDispatch, getState: () => StoreState) => { - if (status === "accepted") dispatch(addToValidCharacters([character])); - else if (status === "rejected") + if (status === CharacterStatus.Accepted) + dispatch(addToValidCharacters([character])); + else if (status === CharacterStatus.Rejected) dispatch(addToRejectedCharacters([character])); - else if (status === "undecided") { + else if (status === CharacterStatus.Undecided) { const state = getState(); const validCharacters = state.characterInventoryState.validCharacters.filter( @@ -121,8 +123,11 @@ export function setCharacterStatus(character: string, status: characterStatus) { export function uploadInventory() { return async (dispatch: StoreStateDispatch, getState: () => StoreState) => { const state = getState(); - const updatedProject = updateCurrentProject(state); - await saveChangesToProject(updatedProject, dispatch); + const changes = getChangesFromState(state); + if (changes.length) { + const updatedProject = updateCurrentProject(state); + await saveChangesToProject(updatedProject, dispatch); + } }; } @@ -178,10 +183,74 @@ export function getCharacterStatus( char: string, validChars: string[], rejectedChars: string[] -): characterStatus { - if (validChars.includes(char)) return "accepted"; - if (rejectedChars.includes(char)) return "rejected"; - return "undecided"; +): CharacterStatus { + if (validChars.includes(char)) { + return CharacterStatus.Accepted; + } + if (rejectedChars.includes(char)) { + return CharacterStatus.Rejected; + } + return CharacterStatus.Undecided; +} + +export type CharacterChange = [string, CharacterStatus, CharacterStatus]; + +function getChangesFromState(state: StoreState): CharacterChange[] { + const proj = state.currentProject; + const charInvState = state.characterInventoryState; + return getChanges(proj, charInvState); +} + +export function getChanges( + proj: Project, + charInvState: CharacterInventoryState +): CharacterChange[] { + const oldAcc = proj.validCharacters; + const newAcc = charInvState.validCharacters; + const oldRej = proj.rejectedCharacters; + const newRej = charInvState.rejectedCharacters; + const allCharacters = [ + ...new Set([...oldAcc, ...newAcc, ...oldRej, ...newRej]), + ]; + const changes: CharacterChange[] = []; + allCharacters.forEach((c) => { + const change = getChange(c, oldAcc, newAcc, oldRej, newRej); + if (change) { + changes.push(change); + } + }); + return changes; +} + +function getChange( + c: string, + oldAcc: string[], + newAcc: string[], + oldRej: string[], + newRej: string[] +): CharacterChange | undefined { + if (oldAcc.includes(c)) { + if (!newAcc.includes(c)) { + if (newRej.includes(c)) { + return [c, CharacterStatus.Accepted, CharacterStatus.Rejected]; + } + return [c, CharacterStatus.Accepted, CharacterStatus.Undecided]; + } + return; + } + if (oldRej.includes(c)) { + if (!newRej.includes(c)) { + if (newAcc.includes(c)) { + return [c, CharacterStatus.Rejected, CharacterStatus.Accepted]; + } + return [c, CharacterStatus.Rejected, CharacterStatus.Undecided]; + } + return; + } + if (newAcc.includes(c)) { + return [c, CharacterStatus.Undecided, CharacterStatus.Accepted]; + } + return [c, CharacterStatus.Undecided, CharacterStatus.Rejected]; } function updateCurrentProject(state: StoreState): Project { diff --git a/src/goals/CharInventoryCreation/CharacterInventoryReducer.tsx b/src/goals/CharInventoryCreation/CharacterInventoryReducer.tsx index c407138489..1ea8e58206 100644 --- a/src/goals/CharInventoryCreation/CharacterInventoryReducer.tsx +++ b/src/goals/CharInventoryCreation/CharacterInventoryReducer.tsx @@ -25,10 +25,14 @@ export const defaultState: CharacterInventoryState = { export interface CharacterSetEntry { character: string; occurrences: number; - status: characterStatus; + status: CharacterStatus; } -export type characterStatus = "accepted" | "undecided" | "rejected"; +export enum CharacterStatus { + Accepted = "accepted", + Rejected = "rejected", + Undecided = "undecided", +} export const characterInventoryReducer = ( state: CharacterInventoryState = defaultState, diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterDetailComponent.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterDetailComponent.tsx index 0979be3e0c..139097dafc 100644 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterDetailComponent.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterDetailComponent.tsx @@ -41,10 +41,10 @@ export default function CharacterDetail(props: CharacterDetailProps) { - + - + diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/CharacterInfoComponent.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo.tsx similarity index 81% rename from src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/CharacterInfoComponent.tsx rename to src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo.tsx index 90326cfe6a..0085c7bccb 100644 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/CharacterInfoComponent.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo.tsx @@ -1,14 +1,19 @@ import { Typography } from "@material-ui/core"; import * as React from "react"; import { Translate } from "react-localize-redux"; +import { useSelector } from "react-redux"; + +import { StoreState } from "types"; interface CharacterInfoProps { character: string; - allWords: string[]; } /** Displays basic information about a character */ export default function CharacterInfo(props: CharacterInfoProps) { + const allWords = useSelector( + (state: StoreState) => state.characterInventoryState.allWords + ); return ( {charToHexValue(props.character)} @@ -16,7 +21,7 @@ export default function CharacterInfo(props: CharacterInfoProps) { [Character details has not been implemented] - {countCharacterOccurences(props.character, props.allWords)}{" "} + {countCharacterOccurences(props.character, allWords)}{" "} diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/index.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/index.tsx deleted file mode 100644 index ec5609168c..0000000000 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from "react-redux"; - -import CharacterInfo from "goals/CharInventoryCreation/components/CharacterDetail/CharacterInfo/CharacterInfoComponent"; -import { StoreState } from "types"; - -function mapStateToProps(state: StoreState) { - return { - allWords: state.characterInventoryState.allWords, - }; -} - -export default connect(mapStateToProps, null)(CharacterInfo); diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx index 5ca1bfa568..acba77246e 100644 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx @@ -3,17 +3,18 @@ import { connect } from "react-redux"; import { StoreStateDispatch } from "types/actions"; import { setCharacterStatus } from "goals/CharInventoryCreation/CharacterInventoryActions"; import CharacterStatusControl from "goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/CharacterStatusControl"; +import { CharacterStatus } from "goals/CharInventoryCreation/CharacterInventoryReducer"; function mapDispatchToProps(dispatch: StoreStateDispatch) { return { accept: (character: string) => { - dispatch(setCharacterStatus(character, "accepted")); + dispatch(setCharacterStatus(character, CharacterStatus.Accepted)); }, reject: (character: string) => { - dispatch(setCharacterStatus(character, "rejected")); + dispatch(setCharacterStatus(character, CharacterStatus.Rejected)); }, unset: (character: string) => { - dispatch(setCharacterStatus(character, "undecided")); + dispatch(setCharacterStatus(character, CharacterStatus.Undecided)); }, }; } diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx new file mode 100644 index 0000000000..15b0851e71 --- /dev/null +++ b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx @@ -0,0 +1,72 @@ +import { Typography } from "@material-ui/core"; +import * as React from "react"; +import { Translate } from "react-localize-redux"; +import { useSelector } from "react-redux"; + +import { StoreState } from "types"; +import { themeColors } from "types/theme"; + +interface CharacterWordsProps { + character: string; +} + +/** Displays words that contain a character */ +export default function CharacterWords(props: CharacterWordsProps) { + const allWords = useSelector( + (state: StoreState) => state.characterInventoryState.allWords + ); + const words = getWordsContainingChar(props.character, allWords, 5); + return ( + + + + + {words.map((word) => ( + + {highlightCharacterInWord(props.character, word)} + + ))} + + ); +} + +function getWordsContainingChar( + character: string, + words: string[], + maxCount: number +): string[] { + const wordsWithChar: string[] = []; + for (const word of words) { + if (word.indexOf(character) !== -1 && !wordsWithChar.includes(word)) { + wordsWithChar.push(word); + if (wordsWithChar.length == maxCount) { + break; + } + } + } + return wordsWithChar; +} + +function highlightCharacterInWord(character: string, word: string) { + const newWord: (JSX.Element | string)[] = []; + for (let i = 0; i < word.length; i++) { + const letter = word[i]; + const key = `${character}_${word}_${i}`; + newWord.push(wordSpan(letter, key, letter === character)); + } + return newWord; +} + +function wordSpan(word: string, key: string, highlight: boolean) { + return ( + + {word} + + ); +} diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/CharacterWordsComponent.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/CharacterWordsComponent.tsx deleted file mode 100644 index 13d30012b7..0000000000 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/CharacterWordsComponent.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Typography } from "@material-ui/core"; -import * as React from "react"; -import { Translate } from "react-localize-redux"; - -import { themeColors } from "types/theme"; - -interface CharacterWordsProps { - character: string; - allWords: string[]; -} - -/** Displays words that contain a character */ -export default function CharacterWords(props: CharacterWordsProps) { - return ( - - - - - {getWordsContainingChar(props.character, props.allWords, 5).map( - (word) => ( - - {highlightCharacterInWord(props.character, word)} - - ) - )} - - ); -} - -function getWordsContainingChar( - character: string, - words: string[], - maxCount: number -) { - let wordsContainingChar = []; - for (let word of words) { - if (word.indexOf(character) !== -1) wordsContainingChar.push(word); - if (wordsContainingChar.length >= maxCount) break; - } - return wordsContainingChar; -} - -function highlightCharacterInWord(character: string, word: string) { - let newWord: (JSX.Element | string)[] = []; - for (const letter of word) { - if (letter === character) - newWord.push( - - {letter} - - ); - else newWord.push(letter); - } - return newWord; -} diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/index.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/index.tsx deleted file mode 100644 index e19cc54dd3..0000000000 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from "react-redux"; - -import CharacterWords from "goals/CharInventoryCreation/components/CharacterDetail/CharacterWords/CharacterWordsComponent"; -import { StoreState } from "types"; - -function mapStateToProps(state: StoreState) { - return { - allWords: state.characterInventoryState.allWords, - }; -} - -export default connect(mapStateToProps, null)(CharacterWords); diff --git a/src/goals/CharInventoryCreation/components/CharacterList/CharacterCard.tsx b/src/goals/CharInventoryCreation/components/CharacterList/CharacterCard.tsx index 29d32ffbe2..3626321613 100644 --- a/src/goals/CharInventoryCreation/components/CharacterList/CharacterCard.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterList/CharacterCard.tsx @@ -7,14 +7,14 @@ import { import React from "react"; import { Translate } from "react-localize-redux"; -import { characterStatus } from "goals/CharInventoryCreation/CharacterInventoryReducer"; +import { CharacterStatus } from "goals/CharInventoryCreation/CharacterInventoryReducer"; import CharacterStatusText from "goals/CharInventoryCreation/components/CharacterList/CharacterStatusText"; import theme from "types/theme"; interface CharacterCardProps { char: string; count: number; - status: characterStatus; + status: CharacterStatus; onClick?: (e: React.MouseEvent) => void; fontHeight: number; cardWidth: number; diff --git a/src/goals/CharInventoryCreation/components/CharacterList/CharacterListComponent.tsx b/src/goals/CharInventoryCreation/components/CharacterList/CharacterListComponent.tsx index 3a5cc23fff..cb805fd724 100644 --- a/src/goals/CharInventoryCreation/components/CharacterList/CharacterListComponent.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterList/CharacterListComponent.tsx @@ -5,6 +5,7 @@ import { MenuItem, Select, } from "@material-ui/core"; +import { ArrowDownward, ArrowUpward } from "@material-ui/icons"; import * as React from "react"; import { LocalizeContextProps, @@ -70,16 +71,20 @@ export class CharacterList extends React.Component< }} > - {this.props.translate("charInventory.characters")} 🡡 + {this.props.translate("charInventory.characters")} + - {this.props.translate("charInventory.characters")} 🡣 + {this.props.translate("charInventory.characters")} + - {this.props.translate("charInventory.occurrences")} 🡡 + {this.props.translate("charInventory.occurrences")} + - {this.props.translate("charInventory.occurrences")} 🡣 + {this.props.translate("charInventory.occurrences")} + {this.props.translate("charInventory.status")} diff --git a/src/goals/CharInventoryCreation/components/CharacterList/CharacterStatusText.tsx b/src/goals/CharInventoryCreation/components/CharacterList/CharacterStatusText.tsx index 8e71721744..c9899cb4c4 100644 --- a/src/goals/CharInventoryCreation/components/CharacterList/CharacterStatusText.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterList/CharacterStatusText.tsx @@ -2,15 +2,15 @@ import Typography from "@material-ui/core/Typography"; import React from "react"; import { Translate } from "react-localize-redux"; -import { characterStatus } from "goals/CharInventoryCreation/CharacterInventoryReducer"; +import { CharacterStatus } from "goals/CharInventoryCreation/CharacterInventoryReducer"; import { themeColors } from "types/theme"; interface CharacterStatusTextProps { - status: characterStatus; + status: CharacterStatus; } export default function CharacterStatusText(props: CharacterStatusTextProps) { - if (props.status === "accepted") { + if (props.status === CharacterStatus.Accepted) { return ( ); - } else if (props.status === "rejected") { + } else if (props.status === CharacterStatus.Rejected) { return ( { } }); -describe("Testing CharacterInventoryActions", () => { +describe("CharacterInventoryActions", () => { test("setInventory yields correct action", () => { - expect(setValidCharacters(VALID_DATA)).toEqual({ - type: CharacterInventoryType.SET_VALID_CHARACTERS, + expect(Actions.setValidCharacters(VALID_DATA)).toEqual({ + type: Actions.CharacterInventoryType.SET_VALID_CHARACTERS, payload: VALID_DATA, }); }); @@ -106,21 +109,53 @@ describe("Testing CharacterInventoryActions", () => { LocalStorage.setCurrentUser(mockUser); LocalStorage.setProjectId(mockProjectId); let mockStore = createMockStore(MOCK_STATE); - const mockUpload = uploadInventory(); + const mockUpload = Actions.uploadInventory(); await mockUpload( mockStore.dispatch, mockStore.getState as () => StoreState ); - expect(backend.updateProject).toHaveBeenCalledTimes(1); + expect(updateProject).toHaveBeenCalledTimes(1); expect(mockStore.getActions()).toEqual([ { type: SET_CURRENT_PROJECT, payload: { - characterSet: null, - validCharacters: VALID_DATA, + characterSet: [], rejectedCharacters: REJECT_DATA, + validCharacters: VALID_DATA, }, }, ]); }); + + test("getChanges returns correct changes", () => { + const accAcc = "accepted"; + const accRej = "accepted->rejected"; + const accUnd = "accepted->undecided"; + const rejAcc = "rejected->accepted"; + const rejRej = "rejected"; + const rejUnd = "rejected->undecided"; + const undAcc = "undecided->accepted"; + const undRej = "undecided->rejected"; + const oldProj = { + ...defaultProject, + validCharacters: [accAcc, accRej, accUnd], + rejectedCharacters: [rejAcc, rejRej, rejUnd], + }; + const charInvState: CharacterInventoryState = { + ...defaultState, + validCharacters: [accAcc, rejAcc, undAcc], + rejectedCharacters: [accRej, rejRej, undRej], + }; + const expectedChanges: Actions.CharacterChange[] = [ + [accRej, CharacterStatus.Accepted, CharacterStatus.Rejected], + [accUnd, CharacterStatus.Accepted, CharacterStatus.Undecided], + [rejAcc, CharacterStatus.Rejected, CharacterStatus.Accepted], + [rejUnd, CharacterStatus.Rejected, CharacterStatus.Undecided], + [undAcc, CharacterStatus.Undecided, CharacterStatus.Accepted], + [undRej, CharacterStatus.Undecided, CharacterStatus.Rejected], + ]; + const changes = Actions.getChanges(oldProj, charInvState); + expect(changes.length).toEqual(expectedChanges.length); + expectedChanges.forEach((ch) => expect(changes).toContainEqual(ch)); + }); }); diff --git a/src/goals/CharInventoryCreation/tests/__snapshots__/CharacterInventoryComponent.test.tsx.snap b/src/goals/CharInventoryCreation/tests/__snapshots__/CharacterInventoryComponent.test.tsx.snap index ea0ad601f1..500a65abd1 100644 --- a/src/goals/CharInventoryCreation/tests/__snapshots__/CharacterInventoryComponent.test.tsx.snap +++ b/src/goals/CharInventoryCreation/tests/__snapshots__/CharacterInventoryComponent.test.tsx.snap @@ -271,7 +271,16 @@ exports[`Character Inventory Component Renders properly (snapshot test) 1`] = ` tabIndex={0} > Missing translationId: charInventory.characters for language: \${ languageCode } - 🡡 + + + Date: Thu, 11 Feb 2021 16:41:07 -0500 Subject: [PATCH 05/11] Fix eslint warnings. --- .../components/CharacterDetail/CharacterWords.tsx | 2 +- src/types/tests/goalUtilities.test.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx index 15b0851e71..20c6b78879 100644 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterWords.tsx @@ -39,7 +39,7 @@ function getWordsContainingChar( for (const word of words) { if (word.indexOf(character) !== -1 && !wordsWithChar.includes(word)) { wordsWithChar.push(word); - if (wordsWithChar.length == maxCount) { + if (wordsWithChar.length === maxCount) { break; } } diff --git a/src/types/tests/goalUtilities.test.tsx b/src/types/tests/goalUtilities.test.tsx index c7a78f14f6..6f21d30e2d 100644 --- a/src/types/tests/goalUtilities.test.tsx +++ b/src/types/tests/goalUtilities.test.tsx @@ -6,7 +6,6 @@ import { convertGoalToEdit, maxNumSteps, } from "types/goalUtilities"; -import { Edit } from "types/userEdit"; describe("goalUtilities", () => { describe("convertGoalToEdit, convertEditToGoal", () => { From 645153642b21ffd5b1d7c12dcdda4ee8508d03a3 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 11 Feb 2021 17:18:24 -0500 Subject: [PATCH 06/11] Fix function that was wrong, but in such a way as to not effect our use. --- .../CharInventoryCreation/CharacterInventoryActions.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx index 0d633d715f..c8b6c3c36c 100644 --- a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx +++ b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx @@ -222,6 +222,7 @@ export function getChanges( return changes; } +// Returns undefined if CharacterStatus unchanged. function getChange( c: string, oldAcc: string[], @@ -250,7 +251,9 @@ function getChange( if (newAcc.includes(c)) { return [c, CharacterStatus.Undecided, CharacterStatus.Accepted]; } - return [c, CharacterStatus.Undecided, CharacterStatus.Rejected]; + if (newRej.includes(c)) { + return [c, CharacterStatus.Undecided, CharacterStatus.Rejected]; + } } function updateCurrentProject(state: StoreState): Project { From 3f85f05b980d623611465cac8a99d8c869195107 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 11 Feb 2021 17:28:46 -0500 Subject: [PATCH 07/11] Sort imports. --- .../CharacterDetail/CharacterStatusControl/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx index acba77246e..029cdb1f00 100644 --- a/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx +++ b/src/goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/index.tsx @@ -1,9 +1,9 @@ import { connect } from "react-redux"; -import { StoreStateDispatch } from "types/actions"; import { setCharacterStatus } from "goals/CharInventoryCreation/CharacterInventoryActions"; -import CharacterStatusControl from "goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/CharacterStatusControl"; import { CharacterStatus } from "goals/CharInventoryCreation/CharacterInventoryReducer"; +import CharacterStatusControl from "goals/CharInventoryCreation/components/CharacterDetail/CharacterStatusControl/CharacterStatusControl"; +import { StoreStateDispatch } from "types/actions"; function mapDispatchToProps(dispatch: StoreStateDispatch) { return { From 51d82aaeefa7ffd3f98fbae4a97c83335ad216d9 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Thu, 11 Feb 2021 17:59:36 -0500 Subject: [PATCH 08/11] Avoid unnecessary dispatches. --- .../CharacterInventoryActions.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx index c8b6c3c36c..4deca0ec30 100644 --- a/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx +++ b/src/goals/CharInventoryCreation/CharacterInventoryActions.tsx @@ -1,13 +1,13 @@ import * as backend from "backend"; import { saveChangesToProject } from "components/Project/ProjectActions"; -import { StoreState } from "types"; -import { StoreStateDispatch } from "types/actions"; -import { Project } from "types/project"; import { CharacterInventoryState, CharacterSetEntry, CharacterStatus, } from "goals/CharInventoryCreation/CharacterInventoryReducer"; +import { StoreState } from "types"; +import { StoreStateDispatch } from "types/actions"; +import { Project } from "types/project"; export enum CharacterInventoryType { SET_VALID_CHARACTERS = "SET_VALID_CHARACTERS", @@ -104,17 +104,15 @@ export function setCharacterStatus(character: string, status: CharacterStatus) { else if (status === CharacterStatus.Rejected) dispatch(addToRejectedCharacters([character])); else if (status === CharacterStatus.Undecided) { - const state = getState(); - - const validCharacters = state.characterInventoryState.validCharacters.filter( - (c) => c !== character - ); - dispatch(setValidCharacters(validCharacters)); - - const rejectedCharacters = state.characterInventoryState.rejectedCharacters.filter( - (c) => c !== character - ); - dispatch(setRejectedCharacters(rejectedCharacters)); + const state = getState().characterInventoryState; + const valid = state.validCharacters.filter((c) => c !== character); + if (valid.length < state.validCharacters.length) { + dispatch(setValidCharacters(valid)); + } + const rejected = state.rejectedCharacters.filter((c) => c !== character); + if (rejected.length < state.rejectedCharacters.length) { + dispatch(setRejectedCharacters(rejected)); + } } }; } From f7c87781517dd147830a2edc4f8e95a8e5ed5bf9 Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Fri, 12 Feb 2021 10:16:40 -0500 Subject: [PATCH 09/11] Use loadingDoneButton to give user feedback on save. --- .../CharacterInventoryComponent.tsx | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/goals/CharInventoryCreation/CharacterInventoryComponent.tsx b/src/goals/CharInventoryCreation/CharacterInventoryComponent.tsx index cb56d8c284..dd4972a062 100644 --- a/src/goals/CharInventoryCreation/CharacterInventoryComponent.tsx +++ b/src/goals/CharInventoryCreation/CharacterInventoryComponent.tsx @@ -12,6 +12,7 @@ import * as React from "react"; import { Translate } from "react-localize-redux"; import history, { Path } from "browserHistory"; +import LoadingDoneButton from "components/Buttons/LoadingDoneButton"; import { CharacterSetEntry } from "goals/CharInventoryCreation/CharacterInventoryReducer"; import CharacterDetail from "goals/CharInventoryCreation/components/CharacterDetail"; import CharacterEntry from "goals/CharInventoryCreation/components/CharacterEntry"; @@ -24,7 +25,7 @@ interface CharacterInventoryProps { setValidCharacters: (inventory: string[]) => void; setRejectedCharacters: (inventory: string[]) => void; setSelectedCharacter: (character: string) => void; - uploadInventory: () => void; + uploadInventory: () => Promise; fetchWords: () => void; currentProject: Project; selectedCharacter: string; @@ -38,6 +39,8 @@ export const CANCEL: string = "cancelInventoryCreation"; interface CharacterInventoryState { cancelDialogOpen: boolean; + saveInProgress: boolean; + saveSuccessful: boolean; } /** @@ -49,7 +52,11 @@ export default class CharacterInventory extends React.Component< > { constructor(props: CharacterInventoryProps) { super(props); - this.state = { cancelDialogOpen: false }; + this.state = { + cancelDialogOpen: false, + saveInProgress: false, + saveSuccessful: false, + }; } componentDidMount() { @@ -66,6 +73,14 @@ export default class CharacterInventory extends React.Component< this.setState({ cancelDialogOpen: false }); } + async save() { + this.setState({ saveInProgress: true }); + await this.props.uploadInventory(); + this.setState({ saveInProgress: false, saveSuccessful: true }); + // Manually pause so user can see save success. + setTimeout(() => this.quit(), 1000); + } + quit() { this.props.resetInState(); history.push(Path.Goals); @@ -103,18 +118,17 @@ export default class CharacterInventory extends React.Component< {/* submission buttons */} - + +