From 25ec43886aa938398fb3166d396915b712558daf Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Thu, 19 Nov 2020 12:12:46 -0500 Subject: [PATCH 01/22] Add first draft of change log; implement in manage.tsx --- src/assets/src/changes.ts | 38 ++++++++++++++++++++++++ src/assets/src/components/changeLog.tsx | 29 ++++++++++++++++++ src/assets/src/components/manage.tsx | 20 ++++++++++--- src/assets/src/hooks/useEntityChanges.ts | 30 +++++++++++++++++++ src/assets/src/hooks/usePreviousState.ts | 12 ++++++++ src/assets/src/models.ts | 11 +++---- src/package-lock.json | 32 ++++++++++++++++++++ src/package.json | 4 +++ src/tsconfig.json | 3 +- 9 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 src/assets/src/changes.ts create mode 100644 src/assets/src/components/changeLog.tsx create mode 100644 src/assets/src/hooks/useEntityChanges.ts create mode 100644 src/assets/src/hooks/usePreviousState.ts diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts new file mode 100644 index 00000000..bbe4a957 --- /dev/null +++ b/src/assets/src/changes.ts @@ -0,0 +1,38 @@ +import xorWith from "lodash.xorwith"; +import isEqual from "lodash.isequal"; + +import { Base } from "./models" + + +export enum EntityType { + queue = 'queue', + meeting = 'meeting' +} + +export interface ChangeEvent { + entityID: number; + message: string; +} + +export interface ChangeEventMap { + [id: number]: ChangeEvent; +} + +// https://lodash.com/docs/4.17.15#xorWith + +export function compareEntities (entityType: EntityType, oldOnes: T[], newOnes: T[]): + ChangeEvent | undefined +{ + const symDiff = xorWith(oldOnes, newOnes, isEqual); + if (symDiff.length === 0) return; + const firstEntity = symDiff[0]; + let message; + if (oldOnes.length < newOnes.length) { + message = `A new ${entityType} with ID number ${firstEntity.id} was added.`; + } else if (oldOnes.length > newOnes.length) { + message = `The ${entityType} with ID number ${firstEntity.id} was deleted.`; + } else { + message = `The ${entityType} with ID number ${firstEntity.id} was changed.`; + } + return { entityID: firstEntity.id, message: message }; +} diff --git a/src/assets/src/components/changeLog.tsx b/src/assets/src/components/changeLog.tsx new file mode 100644 index 00000000..6ee5c7dc --- /dev/null +++ b/src/assets/src/components/changeLog.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { Alert } from "react-bootstrap"; + +import { ChangeEvent, ChangeEventMap } from "../changes"; + + +interface ChangeLogProps { + changeEventMap: ChangeEventMap; + popChangeEvent: (key: number) => void; +} + +export function ChangeLog (props: ChangeLogProps) { + const changeAlerts = Object.keys(props.changeEventMap).map( + (key) => { + const changeEvent: ChangeEvent = props.changeEventMap[Number(key)]; + return ( + props.popChangeEvent(Number(key))} + > + {changeEvent.message} + + ) + } + ) + return
{changeAlerts}
; +} diff --git a/src/assets/src/components/manage.tsx b/src/assets/src/components/manage.tsx index cc4cd878..e2559866 100644 --- a/src/assets/src/components/manage.tsx +++ b/src/assets/src/components/manage.tsx @@ -1,13 +1,17 @@ import * as React from "react"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Link } from "react-router-dom"; import { Button } from "react-bootstrap"; +import { ChangeLog } from "./changeLog"; import { Breadcrumbs, checkForbiddenError, ErrorDisplay, FormError, LoginDialog, QueueTable } from "./common"; import { PageProps } from "./page"; +import { useEntityChanges } from "../hooks/useEntityChanges"; +import { usePreviousState } from "../hooks/usePreviousState"; import { QueueBase } from "../models"; import { useUserWebSocket } from "../services/sockets"; import { redirectToLogin } from "../utils"; +import { EntityType } from "../changes"; interface ManageQueueTableProps { @@ -35,16 +39,23 @@ export function ManagePage(props: PageProps) { if (!props.user) { redirectToLogin(props.loginUrl); } + const [queues, setQueues] = useState(undefined as ReadonlyArray | undefined); + const oldQueues = usePreviousState(queues); const userWebSocketError = useUserWebSocket(props.user!.id, (u) => setQueues(u.hosted_queues)); - + + const [changeEventMap, compareAndSetEventMap, popChangeEvent] = useEntityChanges(EntityType.queue); + useEffect(() => { + if (queues && oldQueues) compareAndSetEventMap(oldQueues, queues); + }, [queues]) + const errorSources = [ {source: 'User Connection', error: userWebSocketError} ].filter(e => e.error) as FormError[]; const loginDialogVisible = errorSources.some(checkForbiddenError); - const errorDisplay = + const errorDisplay = ; const queueTable = queues !== undefined - && + && ; return (
@@ -52,6 +63,7 @@ export function ManagePage(props: PageProps) { {errorDisplay}

My Meeting Queues

These are all the queues you are a host of. Select a queue to manage it or add a queue below.

+ {queueTable}
diff --git a/src/assets/src/hooks/useEntityChanges.ts b/src/assets/src/hooks/useEntityChanges.ts new file mode 100644 index 00000000..28f36a4e --- /dev/null +++ b/src/assets/src/hooks/useEntityChanges.ts @@ -0,0 +1,30 @@ +import { useState } from "react"; + +import { Base } from "../models"; +import { compareEntities, ChangeEventMap, EntityType } from "../changes"; + + +export function useEntityChanges(entityType: EntityType): + [ChangeEventMap, (oldEntities: readonly T[], newEntities: readonly T[]) => void, (key: number) => void] +{ + const [changeEventMap, setChangeEventMap] = useState({} as ChangeEventMap); + const numEvents = Object.keys(changeEventMap).length; + const nextKey = numEvents > 0 ? Number(Object.keys(changeEventMap)[numEvents - 1]) + 1 : 0; + + const compareAndSetEventMap = (oldEntities: readonly T[], newEntities: readonly T[]): void => { + const newChangeEvent = compareEntities(entityType, oldEntities.slice(), newEntities.slice()); + if (newChangeEvent !== undefined) { + const newMap = {} as ChangeEventMap; + newMap[nextKey] = newChangeEvent; + setChangeEventMap(Object.assign({...changeEventMap}, newMap)); + } + } + + const popChangeEvent = (key: number) => { + const newEventMap = {...changeEventMap}; + delete newEventMap[Number(key)]; + setChangeEventMap(newEventMap); + }; + + return [changeEventMap, compareAndSetEventMap, popChangeEvent]; +} diff --git a/src/assets/src/hooks/usePreviousState.ts b/src/assets/src/hooks/usePreviousState.ts new file mode 100644 index 00000000..7fbdd861 --- /dev/null +++ b/src/assets/src/hooks/usePreviousState.ts @@ -0,0 +1,12 @@ +import { useEffect, useRef } from "react"; + + +// https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state + +export function usePreviousState(value: any): any { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} \ No newline at end of file diff --git a/src/assets/src/models.ts b/src/assets/src/models.ts index a45b2222..9c988e63 100644 --- a/src/assets/src/models.ts +++ b/src/assets/src/models.ts @@ -10,8 +10,11 @@ export interface MeetingBackend { intl_telephone_url: string | null; } -export interface User { +export interface Base { id: number; +} + +export interface User extends Base { username: string; first_name: string; last_name: string; @@ -44,8 +47,7 @@ export enum MeetingStatus { STARTED = 2 } -export interface Meeting { - id: number; +export interface Meeting extends Base { line_place: number | null; attendees: User[]; agenda: string; @@ -56,8 +58,7 @@ export interface Meeting { status: MeetingStatus; } -export interface QueueBase { - id: number; +export interface QueueBase extends Base { name: string; status: "open" | "closed"; } diff --git a/src/package-lock.json b/src/package-lock.json index c017a526..01036284 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1556,6 +1556,28 @@ "@types/istanbul-lib-report": "*" } }, + "@types/lodash": { + "version": "4.14.165", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz", + "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==" + }, + "@types/lodash.isequal": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz", + "integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.xorwith": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.xorwith/-/lodash.xorwith-4.5.6.tgz", + "integrity": "sha512-fs13IjIVwzPC8kO+/nzhnyUkuou8gyBY0k1WlZWWGQMc+Un3SJHOixpG9OA+bXLWF6JvEOIBIkuPzQ4AKMHwGQ==", + "requires": { + "@types/lodash": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -8351,6 +8373,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8372,6 +8399,11 @@ "resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz", "integrity": "sha1-xZjErc4YiiflMUVzHNxsDnF3YAw=" }, + "lodash.xorwith": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.xorwith/-/lodash.xorwith-4.5.0.tgz", + "integrity": "sha1-jdFQIzXZatyeRtmlbsO+TvJvwqc=" + }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", diff --git a/src/package.json b/src/package.json index df45695d..ef2b2d87 100644 --- a/src/package.json +++ b/src/package.json @@ -5,6 +5,8 @@ "@fortawesome/fontawesome-svg-core": "^1.2.28", "@fortawesome/free-solid-svg-icons": "^5.13.0", "@fortawesome/react-fontawesome": "^0.1.9", + "lodash.isequal": "^4.5.0", + "lodash.xorwith": "^4.5.0", "react": "^16.10.2", "react-app-polyfill": "~0.2.2", "react-bootstrap": "^1.4.0", @@ -24,6 +26,8 @@ "check-types": "tsc" }, "devDependencies": { + "@types/lodash.isequal": "^4.5.5", + "@types/lodash.xorwith": "^4.5.6", "@types/react": "^16.9.23", "@types/react-dom": "^16.9.5", "@types/react-router": "^5.1.4", diff --git a/src/tsconfig.json b/src/tsconfig.json index eb5fa728..09f0aa7d 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -7,7 +7,8 @@ "esModuleInterop": true, "jsx": "react", "skipLibCheck": true, - "sourceMap": true + "sourceMap": true, + "allowSyntheticDefaultImports": true }, "include": [ "assets/src" From 6ea8a3f6d00f9de500274b680f8448a4a0580300 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Thu, 19 Nov 2020 12:21:50 -0500 Subject: [PATCH 02/22] Tweak syntax, import ordering --- src/assets/src/components/manage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/src/components/manage.tsx b/src/assets/src/components/manage.tsx index e2559866..5f304985 100644 --- a/src/assets/src/components/manage.tsx +++ b/src/assets/src/components/manage.tsx @@ -1,17 +1,17 @@ import * as React from "react"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { Button } from "react-bootstrap"; import { ChangeLog } from "./changeLog"; import { Breadcrumbs, checkForbiddenError, ErrorDisplay, FormError, LoginDialog, QueueTable } from "./common"; import { PageProps } from "./page"; +import { EntityType } from "../changes"; import { useEntityChanges } from "../hooks/useEntityChanges"; import { usePreviousState } from "../hooks/usePreviousState"; import { QueueBase } from "../models"; import { useUserWebSocket } from "../services/sockets"; import { redirectToLogin } from "../utils"; -import { EntityType } from "../changes"; interface ManageQueueTableProps { @@ -55,7 +55,7 @@ export function ManagePage(props: PageProps) { const loginDialogVisible = errorSources.some(checkForbiddenError); const errorDisplay = ; const queueTable = queues !== undefined - && ; + && ; return (
From ca12ebd367a69b915012f56d3afcac9ad8099c96 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Thu, 19 Nov 2020 13:13:15 -0500 Subject: [PATCH 03/22] Implement change log for meetings in queueManager --- src/assets/src/components/queueManager.tsx | 28 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/assets/src/components/queueManager.tsx b/src/assets/src/components/queueManager.tsx index 796fc563..de5007dd 100644 --- a/src/assets/src/components/queueManager.tsx +++ b/src/assets/src/components/queueManager.tsx @@ -1,12 +1,13 @@ import * as React from "react"; -import { useState, createRef, ChangeEvent } from "react"; +import { useEffect, useState, createRef, ChangeEvent } from "react"; import { Link } from "react-router-dom"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCog } from "@fortawesome/free-solid-svg-icons"; import { Alert, Button, Col, Form, InputGroup, Modal, Row } from "react-bootstrap"; import Dialog from "react-bootstrap-dialog"; -import { +import { ChangeLog } from "./changeLog"; +import { UserDisplay, ErrorDisplay, FormError, checkForbiddenError, LoadingDisplay, DateDisplay, CopyField, showConfirmation, LoginDialog, Breadcrumbs, DateTimeDisplay, userLoggedOnWarning } from "./common"; @@ -14,6 +15,9 @@ import { DialInContent } from './dialIn'; import { MeetingsInProgressTable, MeetingsInQueueTable } from "./meetingTables"; import { BackendSelector as MeetingBackendSelector, getBackendByName } from "./meetingType"; import { PageProps } from "./page"; +import { EntityType } from "../changes"; +import { useEntityChanges } from "../hooks/useEntityChanges"; +import { usePreviousState } from "../hooks/usePreviousState"; import { usePromise } from "../hooks/usePromise"; import { useStringValidation } from "../hooks/useValidation"; import { @@ -111,7 +115,7 @@ interface QueueManagerProps { onStartMeeting: (m: Meeting) => void; } -function QueueManager(props: QueueManagerProps) { +function QueueManager (props: React.PropsWithChildren) { const spacingClass = 'mt-4'; let startedMeetings = []; @@ -186,7 +190,10 @@ function QueueManager(props: QueueManagerProps) { - + + {props.children} + +
); @@ -260,6 +267,8 @@ export function QueueManagerPage(props: PageProps) { // Set up basic state const [queue, setQueue] = useState(undefined as QueueHost | undefined); + const oldQueue = usePreviousState(queue); + const [authError, setAuthError] = useState(undefined as Error | undefined); const setQueueChecked = (q: QueueAttendee | QueueHost | undefined) => { if (!q) { @@ -273,6 +282,12 @@ export function QueueManagerPage(props: PageProps) { } } const queueWebSocketError = useQueueWebSocket(queueIdParsed, setQueueChecked); + + const [meetingChangeEventMap, compareAndSetMeetingChangeEventMap, popMeetingChangeEvent] = useEntityChanges(EntityType.meeting); + useEffect(() => { + if (queue && oldQueue) compareAndSetMeetingChangeEventMap(oldQueue.meeting_set, queue.meeting_set); + }, [queue]) + const [visibleMeetingDialog, setVisibleMeetingDialog] = useState(undefined as Meeting | undefined); const [myUser, setMyUser] = useState(undefined as MyUser | undefined); @@ -336,6 +351,7 @@ export function QueueManagerPage(props: PageProps) { const loginDialogVisible = errorSources.some(checkForbiddenError); const loadingDisplay = ; const errorDisplay = ; + const meetingChangeLog = const queueManager = queue && ( ) { onShowMeetingInfo={setVisibleMeetingDialog} onChangeAssignee={doChangeAssignee} onStartMeeting={doStartMeeting} - /> + > + {meetingChangeLog} + ); return ( <> From 4dfd86d89a91566850539da9d80163aed1d3a5ae Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Thu, 19 Nov 2020 13:32:40 -0500 Subject: [PATCH 04/22] Replace EntityType with type checkers --- src/assets/src/changes.ts | 20 +++++++++++++------- src/assets/src/components/manage.tsx | 3 +-- src/assets/src/components/queueManager.tsx | 3 +-- src/assets/src/hooks/useEntityChanges.ts | 6 +++--- src/assets/src/models.ts | 9 +++++++++ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index bbe4a957..7ca29f10 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -1,14 +1,9 @@ import xorWith from "lodash.xorwith"; import isEqual from "lodash.isequal"; -import { Base } from "./models" +import { Base, isQueueBase, isMeeting } from "./models" -export enum EntityType { - queue = 'queue', - meeting = 'meeting' -} - export interface ChangeEvent { entityID: number; message: string; @@ -20,12 +15,23 @@ export interface ChangeEventMap { // https://lodash.com/docs/4.17.15#xorWith -export function compareEntities (entityType: EntityType, oldOnes: T[], newOnes: T[]): +export function compareEntities (oldOnes: T[], newOnes: T[]): ChangeEvent | undefined { const symDiff = xorWith(oldOnes, newOnes, isEqual); if (symDiff.length === 0) return; const firstEntity = symDiff[0]; + + let entityType; + if (isMeeting(firstEntity)) { + entityType = 'meeting'; + } else if (isQueueBase(firstEntity)) { + entityType = 'queue'; + } else { + console.error(`compareEntities was used with an unsupported type: ${firstEntity}`) + return; + } + let message; if (oldOnes.length < newOnes.length) { message = `A new ${entityType} with ID number ${firstEntity.id} was added.`; diff --git a/src/assets/src/components/manage.tsx b/src/assets/src/components/manage.tsx index 5f304985..dc6a8181 100644 --- a/src/assets/src/components/manage.tsx +++ b/src/assets/src/components/manage.tsx @@ -6,7 +6,6 @@ import { Button } from "react-bootstrap"; import { ChangeLog } from "./changeLog"; import { Breadcrumbs, checkForbiddenError, ErrorDisplay, FormError, LoginDialog, QueueTable } from "./common"; import { PageProps } from "./page"; -import { EntityType } from "../changes"; import { useEntityChanges } from "../hooks/useEntityChanges"; import { usePreviousState } from "../hooks/usePreviousState"; import { QueueBase } from "../models"; @@ -44,7 +43,7 @@ export function ManagePage(props: PageProps) { const oldQueues = usePreviousState(queues); const userWebSocketError = useUserWebSocket(props.user!.id, (u) => setQueues(u.hosted_queues)); - const [changeEventMap, compareAndSetEventMap, popChangeEvent] = useEntityChanges(EntityType.queue); + const [changeEventMap, compareAndSetEventMap, popChangeEvent] = useEntityChanges(); useEffect(() => { if (queues && oldQueues) compareAndSetEventMap(oldQueues, queues); }, [queues]) diff --git a/src/assets/src/components/queueManager.tsx b/src/assets/src/components/queueManager.tsx index de5007dd..32b8afd2 100644 --- a/src/assets/src/components/queueManager.tsx +++ b/src/assets/src/components/queueManager.tsx @@ -15,7 +15,6 @@ import { DialInContent } from './dialIn'; import { MeetingsInProgressTable, MeetingsInQueueTable } from "./meetingTables"; import { BackendSelector as MeetingBackendSelector, getBackendByName } from "./meetingType"; import { PageProps } from "./page"; -import { EntityType } from "../changes"; import { useEntityChanges } from "../hooks/useEntityChanges"; import { usePreviousState } from "../hooks/usePreviousState"; import { usePromise } from "../hooks/usePromise"; @@ -283,7 +282,7 @@ export function QueueManagerPage(props: PageProps) { } const queueWebSocketError = useQueueWebSocket(queueIdParsed, setQueueChecked); - const [meetingChangeEventMap, compareAndSetMeetingChangeEventMap, popMeetingChangeEvent] = useEntityChanges(EntityType.meeting); + const [meetingChangeEventMap, compareAndSetMeetingChangeEventMap, popMeetingChangeEvent] = useEntityChanges(); useEffect(() => { if (queue && oldQueue) compareAndSetMeetingChangeEventMap(oldQueue.meeting_set, queue.meeting_set); }, [queue]) diff --git a/src/assets/src/hooks/useEntityChanges.ts b/src/assets/src/hooks/useEntityChanges.ts index 28f36a4e..ed64346d 100644 --- a/src/assets/src/hooks/useEntityChanges.ts +++ b/src/assets/src/hooks/useEntityChanges.ts @@ -1,10 +1,10 @@ import { useState } from "react"; import { Base } from "../models"; -import { compareEntities, ChangeEventMap, EntityType } from "../changes"; +import { compareEntities, ChangeEventMap } from "../changes"; -export function useEntityChanges(entityType: EntityType): +export function useEntityChanges(): [ChangeEventMap, (oldEntities: readonly T[], newEntities: readonly T[]) => void, (key: number) => void] { const [changeEventMap, setChangeEventMap] = useState({} as ChangeEventMap); @@ -12,7 +12,7 @@ export function useEntityChanges(entityType: EntityType): const nextKey = numEvents > 0 ? Number(Object.keys(changeEventMap)[numEvents - 1]) + 1 : 0; const compareAndSetEventMap = (oldEntities: readonly T[], newEntities: readonly T[]): void => { - const newChangeEvent = compareEntities(entityType, oldEntities.slice(), newEntities.slice()); + const newChangeEvent = compareEntities(oldEntities.slice(), newEntities.slice()); if (newChangeEvent !== undefined) { const newMap = {} as ChangeEventMap; newMap[nextKey] = newChangeEvent; diff --git a/src/assets/src/models.ts b/src/assets/src/models.ts index 9c988e63..1626784b 100644 --- a/src/assets/src/models.ts +++ b/src/assets/src/models.ts @@ -58,11 +58,20 @@ export interface Meeting extends Base { status: MeetingStatus; } +export const isMeeting = (entity: object): entity is Meeting => { + return 'attendees' in entity; +} + + export interface QueueBase extends Base { name: string; status: "open" | "closed"; } +export const isQueueBase = (entity: object): entity is QueueBase => { + return 'name' in entity && 'status' in entity; +} + export interface QueueFull extends QueueBase { created_at: string; description: string; From ee5966a62c0c3311f7f8f4ec88e5539354a0a8fa Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Thu, 19 Nov 2020 13:34:39 -0500 Subject: [PATCH 05/22] Use more specific var names with useEntityChanges in manage.tsx --- src/assets/src/components/manage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/src/components/manage.tsx b/src/assets/src/components/manage.tsx index dc6a8181..2a68807b 100644 --- a/src/assets/src/components/manage.tsx +++ b/src/assets/src/components/manage.tsx @@ -43,9 +43,9 @@ export function ManagePage(props: PageProps) { const oldQueues = usePreviousState(queues); const userWebSocketError = useUserWebSocket(props.user!.id, (u) => setQueues(u.hosted_queues)); - const [changeEventMap, compareAndSetEventMap, popChangeEvent] = useEntityChanges(); + const [queueChangeEventMap, compareAndSetQueueChangeEventMap, popQueueChangeEvent] = useEntityChanges(); useEffect(() => { - if (queues && oldQueues) compareAndSetEventMap(oldQueues, queues); + if (queues && oldQueues) compareAndSetQueueChangeEventMap(oldQueues, queues); }, [queues]) const errorSources = [ @@ -62,7 +62,7 @@ export function ManagePage(props: PageProps) { {errorDisplay}

My Meeting Queues

These are all the queues you are a host of. Select a queue to manage it or add a queue below.

- + {queueTable}
From 41e7c6a4861d86aa823e959c55f619c7a9a46179 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Mon, 23 Nov 2020 17:44:55 -0500 Subject: [PATCH 06/22] Modify identifier for meetings; implement detectChanges --- src/assets/src/changes.ts | 53 ++++++++++++++++++++++++++++++++++++--- src/assets/src/models.ts | 5 ++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index 7ca29f10..d8d8f263 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -1,7 +1,7 @@ import xorWith from "lodash.xorwith"; import isEqual from "lodash.isequal"; -import { Base, isQueueBase, isMeeting } from "./models" +import { Base, isMeeting, isQueueBase, isUser, Meeting, QueueBase } from "./models" export interface ChangeEvent { @@ -13,6 +13,35 @@ export interface ChangeEventMap { [id: number]: ChangeEvent; } +const queueBasePropsToWatch: (keyof QueueBase)[] = ['status', 'name']; +const meetingPropsToWatch: (keyof Meeting)[] = ['backend_type', 'assignee']; + +interface HumanReadableMap { + [key: string]: string; +} + +const propertyMap: HumanReadableMap = { + 'backend_type': 'meeting type', + 'assignee': 'host' +} + +function detectChanges(versOne: T, versTwo: T, propsToWatch: (keyof T)[]): string { + for (const property of propsToWatch) { + if (versOne[property] !== versTwo[property]) { + let valueOne = versOne[property] as T[keyof T] | string; + let valueTwo = versTwo[property] as T[keyof T] | string; + // Check for nested user objects + if (isUser(valueOne)) valueOne = valueOne.username; + if (isUser(valueTwo)) valueTwo = valueTwo.username; + // Make some property strings more human readable + const propName = (property in propertyMap) ? propertyMap[property as string] : property; + return `The ${propName} changed from "${valueOne}" to "${valueTwo}".`; + } + } + return ''; +} + + // https://lodash.com/docs/4.17.15#xorWith export function compareEntities (oldOnes: T[], newOnes: T[]): @@ -21,12 +50,17 @@ export function compareEntities (oldOnes: T[], newOnes: T[]): const symDiff = xorWith(oldOnes, newOnes, isEqual); if (symDiff.length === 0) return; const firstEntity = symDiff[0]; + const secondEntity = symDiff.length > 1 ? symDiff[1] : undefined; let entityType; + let permIdentifier; if (isMeeting(firstEntity)) { entityType = 'meeting'; + // meeting.attendees may change in the future? + permIdentifier = `attendee ${firstEntity.attendees[0].username}`; } else if (isQueueBase(firstEntity)) { entityType = 'queue'; + permIdentifier = `ID number ${firstEntity.id}`; } else { console.error(`compareEntities was used with an unsupported type: ${firstEntity}`) return; @@ -34,11 +68,22 @@ export function compareEntities (oldOnes: T[], newOnes: T[]): let message; if (oldOnes.length < newOnes.length) { - message = `A new ${entityType} with ID number ${firstEntity.id} was added.`; + message = `A new ${entityType} with ${permIdentifier} was added.`; } else if (oldOnes.length > newOnes.length) { - message = `The ${entityType} with ID number ${firstEntity.id} was deleted.`; + message = `The ${entityType} with ${permIdentifier} was deleted.`; } else { - message = `The ${entityType} with ID number ${firstEntity.id} was changed.`; + let changeDetected; + if (secondEntity) { + if (isMeeting(firstEntity) && isMeeting(secondEntity)) { + changeDetected = detectChanges(firstEntity, secondEntity, meetingPropsToWatch); + } else if (isQueueBase(firstEntity) && isQueueBase(secondEntity)) { + changeDetected = detectChanges(firstEntity, secondEntity, queueBasePropsToWatch); + } + } + message = `The ${entityType} with ${permIdentifier} was changed.`; + if (changeDetected) { + message = message + ' ' + changeDetected; + } } return { entityID: firstEntity.id, message: message }; } diff --git a/src/assets/src/models.ts b/src/assets/src/models.ts index 1626784b..696dbc4c 100644 --- a/src/assets/src/models.ts +++ b/src/assets/src/models.ts @@ -22,6 +22,11 @@ export interface User extends Base { hosted_queues?: ReadonlyArray; } +export const isUser = (value: any): value is User => { + if (!value || typeof value !== 'object') return false; + return 'username' in value && 'first_name' in value && 'last_name' in value; +} + export interface MyUser extends User { my_queue: QueueAttendee | null; phone_number: string; From 2b0fc590ab9d5cbfc1df4ea31b57620615fcfe5b Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Tue, 24 Nov 2020 11:42:10 -0500 Subject: [PATCH 07/22] Switch to using ChangeEvents[] instead of ChangeEventMap --- src/assets/src/changes.ts | 13 ++++------ src/assets/src/components/changeLog.tsx | 15 ++++++------ src/assets/src/components/manage.tsx | 8 +++---- src/assets/src/components/queueManager.tsx | 8 +++---- src/assets/src/hooks/useEntityChanges.ts | 28 ++++++++++------------ 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index d8d8f263..b34239b3 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -5,12 +5,8 @@ import { Base, isMeeting, isQueueBase, isUser, Meeting, QueueBase } from "./mode export interface ChangeEvent { - entityID: number; - message: string; -} - -export interface ChangeEventMap { - [id: number]: ChangeEvent; + eventID: number; + text: string; } const queueBasePropsToWatch: (keyof QueueBase)[] = ['status', 'name']; @@ -44,8 +40,7 @@ function detectChanges(versOne: T, versTwo: T, propsToWatch: (ke // https://lodash.com/docs/4.17.15#xorWith -export function compareEntities (oldOnes: T[], newOnes: T[]): - ChangeEvent | undefined +export function compareEntities (oldOnes: T[], newOnes: T[]): string | undefined { const symDiff = xorWith(oldOnes, newOnes, isEqual); if (symDiff.length === 0) return; @@ -85,5 +80,5 @@ export function compareEntities (oldOnes: T[], newOnes: T[]): message = message + ' ' + changeDetected; } } - return { entityID: firstEntity.id, message: message }; + return message; } diff --git a/src/assets/src/components/changeLog.tsx b/src/assets/src/components/changeLog.tsx index 6ee5c7dc..1eea9ea6 100644 --- a/src/assets/src/components/changeLog.tsx +++ b/src/assets/src/components/changeLog.tsx @@ -1,26 +1,25 @@ import * as React from "react"; import { Alert } from "react-bootstrap"; -import { ChangeEvent, ChangeEventMap } from "../changes"; +import { ChangeEvent } from "../changes"; interface ChangeLogProps { - changeEventMap: ChangeEventMap; + changeEvents: ChangeEvent[]; popChangeEvent: (key: number) => void; } export function ChangeLog (props: ChangeLogProps) { - const changeAlerts = Object.keys(props.changeEventMap).map( - (key) => { - const changeEvent: ChangeEvent = props.changeEventMap[Number(key)]; + const changeAlerts = props.changeEvents.map( + (e) => { return ( props.popChangeEvent(Number(key))} + onClose={() => props.popChangeEvent(e.eventID)} > - {changeEvent.message} + {e.text} ) } diff --git a/src/assets/src/components/manage.tsx b/src/assets/src/components/manage.tsx index 2a68807b..7e7e6e3f 100644 --- a/src/assets/src/components/manage.tsx +++ b/src/assets/src/components/manage.tsx @@ -43,10 +43,10 @@ export function ManagePage(props: PageProps) { const oldQueues = usePreviousState(queues); const userWebSocketError = useUserWebSocket(props.user!.id, (u) => setQueues(u.hosted_queues)); - const [queueChangeEventMap, compareAndSetQueueChangeEventMap, popQueueChangeEvent] = useEntityChanges(); + const [queueChangeEvents, compareAndSetChangeEvents, popQueueChangeEvent] = useEntityChanges(); useEffect(() => { - if (queues && oldQueues) compareAndSetQueueChangeEventMap(oldQueues, queues); - }, [queues]) + if (queues && oldQueues) compareAndSetChangeEvents(oldQueues, queues); + }, [queues]); const errorSources = [ {source: 'User Connection', error: userWebSocketError} @@ -62,7 +62,7 @@ export function ManagePage(props: PageProps) { {errorDisplay}

My Meeting Queues

These are all the queues you are a host of. Select a queue to manage it or add a queue below.

- + {queueTable}
diff --git a/src/assets/src/components/queueManager.tsx b/src/assets/src/components/queueManager.tsx index 32b8afd2..76078769 100644 --- a/src/assets/src/components/queueManager.tsx +++ b/src/assets/src/components/queueManager.tsx @@ -282,10 +282,10 @@ export function QueueManagerPage(props: PageProps) { } const queueWebSocketError = useQueueWebSocket(queueIdParsed, setQueueChecked); - const [meetingChangeEventMap, compareAndSetMeetingChangeEventMap, popMeetingChangeEvent] = useEntityChanges(); + const [meetingChangeEvents, compareAndSetMeetingChangeEvents, popMeetingChangeEvent] = useEntityChanges(); useEffect(() => { - if (queue && oldQueue) compareAndSetMeetingChangeEventMap(oldQueue.meeting_set, queue.meeting_set); - }, [queue]) + if (queue && oldQueue) compareAndSetMeetingChangeEvents(oldQueue.meeting_set, queue.meeting_set); + }, [queue]); const [visibleMeetingDialog, setVisibleMeetingDialog] = useState(undefined as Meeting | undefined); @@ -350,7 +350,7 @@ export function QueueManagerPage(props: PageProps) { const loginDialogVisible = errorSources.some(checkForbiddenError); const loadingDisplay = ; const errorDisplay = ; - const meetingChangeLog = + const meetingChangeLog = ; const queueManager = queue && ( (): - [ChangeEventMap, (oldEntities: readonly T[], newEntities: readonly T[]) => void, (key: number) => void] + [ChangeEvent[], (oldEntities: readonly T[], newEntities: readonly T[]) => void, (key: number) => void] { - const [changeEventMap, setChangeEventMap] = useState({} as ChangeEventMap); - const numEvents = Object.keys(changeEventMap).length; - const nextKey = numEvents > 0 ? Number(Object.keys(changeEventMap)[numEvents - 1]) + 1 : 0; + const [changeEvents, setChangeEvents] = useState([] as ChangeEvent[]); + const [nextID, setNextID] = useState(0); const compareAndSetEventMap = (oldEntities: readonly T[], newEntities: readonly T[]): void => { - const newChangeEvent = compareEntities(oldEntities.slice(), newEntities.slice()); - if (newChangeEvent !== undefined) { - const newMap = {} as ChangeEventMap; - newMap[nextKey] = newChangeEvent; - setChangeEventMap(Object.assign({...changeEventMap}, newMap)); + const changeText = compareEntities(oldEntities.slice(), newEntities.slice()); + if (changeText !== undefined) { + const newChangeEvent = { eventID: nextID, text: changeText } as ChangeEvent; + setChangeEvents([...changeEvents, newChangeEvent]); + setNextID(nextID + 1); } } - const popChangeEvent = (key: number) => { - const newEventMap = {...changeEventMap}; - delete newEventMap[Number(key)]; - setChangeEventMap(newEventMap); + const popChangeEvent = (id: number) => { + const newArray = changeEvents.filter((e) => id !== e.eventID); + setChangeEvents(newArray); }; - return [changeEventMap, compareAndSetEventMap, popChangeEvent]; + return [changeEvents, compareAndSetEventMap, popChangeEvent]; } From bcf670294ce43e49f8fa1f840c296f1d2d3e6224 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Tue, 24 Nov 2020 13:18:38 -0500 Subject: [PATCH 08/22] Refactor deleteChangeEvent; implement TimedChangeAlert --- src/assets/src/components/changeLog.tsx | 40 ++++++++++++++-------- src/assets/src/components/manage.tsx | 4 +-- src/assets/src/components/queueManager.tsx | 4 +-- src/assets/src/hooks/useEntityChanges.ts | 10 +++--- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/assets/src/components/changeLog.tsx b/src/assets/src/components/changeLog.tsx index 1eea9ea6..2128573f 100644 --- a/src/assets/src/components/changeLog.tsx +++ b/src/assets/src/components/changeLog.tsx @@ -1,28 +1,40 @@ import * as React from "react"; +import { useEffect } from "react"; import { Alert } from "react-bootstrap"; import { ChangeEvent } from "../changes"; +// https://stackoverflow.com/a/56777520 + +interface TimedChangeAlertProps { + changeEvent: ChangeEvent, + deleteChangeEvent: (id: number) => void; +} + +function TimedChangeAlert (props: TimedChangeAlertProps) { + const deleteEvent = () => props.deleteChangeEvent(props.changeEvent.eventID); + + useEffect(() => { + const timeoutID = setTimeout(deleteEvent, 7000); + return () => clearTimeout(timeoutID); + }, []); + + return ( + + {props.changeEvent.text} + + ); +} + interface ChangeLogProps { changeEvents: ChangeEvent[]; - popChangeEvent: (key: number) => void; + deleteChangeEvent: (id: number) => void; } export function ChangeLog (props: ChangeLogProps) { const changeAlerts = props.changeEvents.map( - (e) => { - return ( - props.popChangeEvent(e.eventID)} - > - {e.text} - - ) - } - ) + (e) => + ); return
{changeAlerts}
; } diff --git a/src/assets/src/components/manage.tsx b/src/assets/src/components/manage.tsx index 7e7e6e3f..a8c6d794 100644 --- a/src/assets/src/components/manage.tsx +++ b/src/assets/src/components/manage.tsx @@ -43,7 +43,7 @@ export function ManagePage(props: PageProps) { const oldQueues = usePreviousState(queues); const userWebSocketError = useUserWebSocket(props.user!.id, (u) => setQueues(u.hosted_queues)); - const [queueChangeEvents, compareAndSetChangeEvents, popQueueChangeEvent] = useEntityChanges(); + const [queueChangeEvents, compareAndSetChangeEvents, deleteQueueChangeEvent] = useEntityChanges(); useEffect(() => { if (queues && oldQueues) compareAndSetChangeEvents(oldQueues, queues); }, [queues]); @@ -62,7 +62,7 @@ export function ManagePage(props: PageProps) { {errorDisplay}

My Meeting Queues

These are all the queues you are a host of. Select a queue to manage it or add a queue below.

- + {queueTable}
diff --git a/src/assets/src/components/queueManager.tsx b/src/assets/src/components/queueManager.tsx index 76078769..c6247ae3 100644 --- a/src/assets/src/components/queueManager.tsx +++ b/src/assets/src/components/queueManager.tsx @@ -282,7 +282,7 @@ export function QueueManagerPage(props: PageProps) { } const queueWebSocketError = useQueueWebSocket(queueIdParsed, setQueueChecked); - const [meetingChangeEvents, compareAndSetMeetingChangeEvents, popMeetingChangeEvent] = useEntityChanges(); + const [meetingChangeEvents, compareAndSetMeetingChangeEvents, deleteMeetingChangeEvent] = useEntityChanges(); useEffect(() => { if (queue && oldQueue) compareAndSetMeetingChangeEvents(oldQueue.meeting_set, queue.meeting_set); }, [queue]); @@ -350,7 +350,7 @@ export function QueueManagerPage(props: PageProps) { const loginDialogVisible = errorSources.some(checkForbiddenError); const loadingDisplay = ; const errorDisplay = ; - const meetingChangeLog = ; + const meetingChangeLog = ; const queueManager = queue && ( (): setChangeEvents([...changeEvents, newChangeEvent]); setNextID(nextID + 1); } - } + }; - const popChangeEvent = (id: number) => { - const newArray = changeEvents.filter((e) => id !== e.eventID); - setChangeEvents(newArray); + // https://reactjs.org/docs/hooks-reference.html#functional-updates + const deleteChangeEvent = (id: number) => { + setChangeEvents((prevChangeEvents) => prevChangeEvents.filter((e) => id !== e.eventID)); }; - return [changeEvents, compareAndSetEventMap, popChangeEvent]; + return [changeEvents, compareAndSetEventMap, deleteChangeEvent]; } From 1e3ce879edb11f7e1bcceb3b3b8dd86e6e307258 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Tue, 1 Dec 2020 11:52:32 -0500 Subject: [PATCH 09/22] Add aria-live property to Alerts --- src/assets/src/components/changeLog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/src/components/changeLog.tsx b/src/assets/src/components/changeLog.tsx index 2128573f..220b9c78 100644 --- a/src/assets/src/components/changeLog.tsx +++ b/src/assets/src/components/changeLog.tsx @@ -21,7 +21,7 @@ function TimedChangeAlert (props: TimedChangeAlertProps) { }, []); return ( - + {props.changeEvent.text} ); From 03b94ae92f10949c5bb8025b377781400377eef4 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 9 Dec 2020 11:27:19 -0500 Subject: [PATCH 10/22] Change location of meeting ChangeLog --- src/assets/src/components/queueManager.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/assets/src/components/queueManager.tsx b/src/assets/src/components/queueManager.tsx index c6247ae3..80a87acb 100644 --- a/src/assets/src/components/queueManager.tsx +++ b/src/assets/src/components/queueManager.tsx @@ -170,6 +170,7 @@ function QueueManager (props: React.PropsWithChildren) {
Created
+ {props.children}

Meetings in Progress

{cannotReassignHostWarning} @@ -189,10 +190,7 @@ function QueueManager (props: React.PropsWithChildren) { - - {props.children} - - +
); From a5f546d1013b553521a6f696f26c0a248269f08c Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 9 Dec 2020 11:51:31 -0500 Subject: [PATCH 11/22] Use ChangeLog inside of QueueManager instead of using props.children --- src/assets/src/components/queueManager.tsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/assets/src/components/queueManager.tsx b/src/assets/src/components/queueManager.tsx index 80a87acb..fae3038e 100644 --- a/src/assets/src/components/queueManager.tsx +++ b/src/assets/src/components/queueManager.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { useEffect, useState, createRef, ChangeEvent } from "react"; +import { useEffect, useState, createRef, ChangeEvent as ReactChangeEvent } from "react"; import { Link } from "react-router-dom"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCog } from "@fortawesome/free-solid-svg-icons"; @@ -15,6 +15,7 @@ import { DialInContent } from './dialIn'; import { MeetingsInProgressTable, MeetingsInQueueTable } from "./meetingTables"; import { BackendSelector as MeetingBackendSelector, getBackendByName } from "./meetingType"; import { PageProps } from "./page"; +import { ChangeEvent as EntityChangeEvent } from "../changes"; import { useEntityChanges } from "../hooks/useEntityChanges"; import { usePreviousState } from "../hooks/usePreviousState"; import { usePromise } from "../hooks/usePromise"; @@ -112,6 +113,8 @@ interface QueueManagerProps { onShowMeetingInfo: (m: Meeting) => void; onChangeAssignee: (a: User | undefined, m: Meeting) => void; onStartMeeting: (m: Meeting) => void; + meetingChangeEvents: EntityChangeEvent[]; + deleteMeetingChangeEvent: (key: number) => void; } function QueueManager (props: React.PropsWithChildren) { @@ -162,7 +165,7 @@ function QueueManager (props: React.PropsWithChildren) { type='switch' label={currentStatus ? 'Open' : 'Closed'} checked={props.queue.status === 'open'} - onChange={(e: ChangeEvent) => props.onSetStatus(!currentStatus)} + onChange={(e: ReactChangeEvent) => props.onSetStatus(!currentStatus)} /> @@ -170,7 +173,11 @@ function QueueManager (props: React.PropsWithChildren) {
Created
- {props.children} + + + + +

Meetings in Progress

{cannotReassignHostWarning} @@ -348,7 +355,6 @@ export function QueueManagerPage(props: PageProps) { const loginDialogVisible = errorSources.some(checkForbiddenError); const loadingDisplay = ; const errorDisplay = ; - const meetingChangeLog = ; const queueManager = queue && ( ) { onShowMeetingInfo={setVisibleMeetingDialog} onChangeAssignee={doChangeAssignee} onStartMeeting={doStartMeeting} - > - {meetingChangeLog} - + meetingChangeEvents={meetingChangeEvents} + deleteMeetingChangeEvent={deleteMeetingChangeEvent} + /> ); return ( <> From 2a4b612dff24c053b2016b3af2f15da216106a4b Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 9 Dec 2020 12:25:30 -0500 Subject: [PATCH 12/22] Compare values in detectChanges after user object handling; only announce changes if detectChanges finds something --- src/assets/src/changes.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index b34239b3..b717d462 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -1,7 +1,7 @@ import xorWith from "lodash.xorwith"; import isEqual from "lodash.isequal"; -import { Base, isMeeting, isQueueBase, isUser, Meeting, QueueBase } from "./models" +import { Base, isMeeting, isQueueBase, isUser, Meeting, MeetingStatus, QueueBase } from "./models" export interface ChangeEvent { @@ -23,12 +23,12 @@ const propertyMap: HumanReadableMap = { function detectChanges(versOne: T, versTwo: T, propsToWatch: (keyof T)[]): string { for (const property of propsToWatch) { - if (versOne[property] !== versTwo[property]) { - let valueOne = versOne[property] as T[keyof T] | string; - let valueTwo = versTwo[property] as T[keyof T] | string; - // Check for nested user objects - if (isUser(valueOne)) valueOne = valueOne.username; - if (isUser(valueTwo)) valueTwo = valueTwo.username; + let valueOne = versOne[property] as T[keyof T] | string; + let valueTwo = versTwo[property] as T[keyof T] | string; + // Check for nested user objects + if (isUser(valueOne)) valueOne = valueOne.username; + if (isUser(valueTwo)) valueTwo = valueTwo.username; + if (valueOne !== valueTwo) { // Make some property strings more human readable const propName = (property in propertyMap) ? propertyMap[property as string] : property; return `The ${propName} changed from "${valueOne}" to "${valueTwo}".`; @@ -63,9 +63,9 @@ export function compareEntities (oldOnes: T[], newOnes: T[]): st let message; if (oldOnes.length < newOnes.length) { - message = `A new ${entityType} with ${permIdentifier} was added.`; + return `A new ${entityType} with ${permIdentifier} was added.`; } else if (oldOnes.length > newOnes.length) { - message = `The ${entityType} with ${permIdentifier} was deleted.`; + return `The ${entityType} with ${permIdentifier} was deleted.`; } else { let changeDetected; if (secondEntity) { @@ -78,7 +78,8 @@ export function compareEntities (oldOnes: T[], newOnes: T[]): st message = `The ${entityType} with ${permIdentifier} was changed.`; if (changeDetected) { message = message + ' ' + changeDetected; + return message; } } - return message; + return; } From d16e7710b0b1b85c10cdba44f813cfa7a25978c8 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 9 Dec 2020 12:27:13 -0500 Subject: [PATCH 13/22] Reassign falsy values to "None" --- src/assets/src/changes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index b717d462..19beba91 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -25,9 +25,11 @@ function detectChanges(versOne: T, versTwo: T, propsToWatch: (ke for (const property of propsToWatch) { let valueOne = versOne[property] as T[keyof T] | string; let valueTwo = versTwo[property] as T[keyof T] | string; - // Check for nested user objects + // Check for nested user objects and falsy values if (isUser(valueOne)) valueOne = valueOne.username; + if (!valueOne) valueOne = 'None'; if (isUser(valueTwo)) valueTwo = valueTwo.username; + if (!valueTwo) valueTwo = 'None'; if (valueOne !== valueTwo) { // Make some property strings more human readable const propName = (property in propertyMap) ? propertyMap[property as string] : property; From 6bbbb7c28eed5274c4a00fca30e53181c13ba99b Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 9 Dec 2020 12:28:58 -0500 Subject: [PATCH 14/22] Add custom check for whether meeting has changed to in progress --- src/assets/src/changes.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index 19beba91..0495b58e 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -73,6 +73,9 @@ export function compareEntities (oldOnes: T[], newOnes: T[]): st if (secondEntity) { if (isMeeting(firstEntity) && isMeeting(secondEntity)) { changeDetected = detectChanges(firstEntity, secondEntity, meetingPropsToWatch); + if (!changeDetected && firstEntity.status !== secondEntity.status && secondEntity.status === MeetingStatus.STARTED) { + changeDetected = 'The status indicates the meeting is now in progress.'; + } } else if (isQueueBase(firstEntity) && isQueueBase(secondEntity)) { changeDetected = detectChanges(firstEntity, secondEntity, queueBasePropsToWatch); } From c698316e98b86b9efd3897bc74d0758d2e0a02e0 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 9 Dec 2020 12:32:28 -0500 Subject: [PATCH 15/22] Tweak return value of detectChanges --- src/assets/src/changes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index 0495b58e..ed555d84 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -21,7 +21,7 @@ const propertyMap: HumanReadableMap = { 'assignee': 'host' } -function detectChanges(versOne: T, versTwo: T, propsToWatch: (keyof T)[]): string { +function detectChanges(versOne: T, versTwo: T, propsToWatch: (keyof T)[]): string | undefined { for (const property of propsToWatch) { let valueOne = versOne[property] as T[keyof T] | string; let valueTwo = versTwo[property] as T[keyof T] | string; @@ -36,7 +36,7 @@ function detectChanges(versOne: T, versTwo: T, propsToWatch: (ke return `The ${propName} changed from "${valueOne}" to "${valueTwo}".`; } } - return ''; + return; } From 54f18fc138a4de57f7a13930b6aa50795dae60b4 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 9 Dec 2020 15:40:34 -0500 Subject: [PATCH 16/22] Fix a few misc. issues --- src/assets/src/components/queueManager.tsx | 2 +- src/assets/src/hooks/usePreviousState.ts | 2 +- src/assets/src/models.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/assets/src/components/queueManager.tsx b/src/assets/src/components/queueManager.tsx index fae3038e..d7cace9c 100644 --- a/src/assets/src/components/queueManager.tsx +++ b/src/assets/src/components/queueManager.tsx @@ -117,7 +117,7 @@ interface QueueManagerProps { deleteMeetingChangeEvent: (key: number) => void; } -function QueueManager (props: React.PropsWithChildren) { +function QueueManager (props: QueueManagerProps) { const spacingClass = 'mt-4'; let startedMeetings = []; diff --git a/src/assets/src/hooks/usePreviousState.ts b/src/assets/src/hooks/usePreviousState.ts index 7fbdd861..8a30d2fa 100644 --- a/src/assets/src/hooks/usePreviousState.ts +++ b/src/assets/src/hooks/usePreviousState.ts @@ -9,4 +9,4 @@ export function usePreviousState(value: any): any { ref.current = value; }); return ref.current; -} \ No newline at end of file +} diff --git a/src/assets/src/models.ts b/src/assets/src/models.ts index 696dbc4c..2b070786 100644 --- a/src/assets/src/models.ts +++ b/src/assets/src/models.ts @@ -67,7 +67,6 @@ export const isMeeting = (entity: object): entity is Meeting => { return 'attendees' in entity; } - export interface QueueBase extends Base { name: string; status: "open" | "closed"; From 1bd1d2049a12b02e0afe53df5f52c64325e85405 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Tue, 5 Jan 2021 16:09:51 -0500 Subject: [PATCH 17/22] Use ComparableEntity with generic instead of Base --- src/assets/src/changes.ts | 7 ++++--- src/assets/src/hooks/useEntityChanges.ts | 5 ++--- src/assets/src/models.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index ed555d84..459b5c05 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -1,8 +1,9 @@ import xorWith from "lodash.xorwith"; import isEqual from "lodash.isequal"; -import { Base, isMeeting, isQueueBase, isUser, Meeting, MeetingStatus, QueueBase } from "./models" +import { isMeeting, isQueueBase, isUser, Meeting, MeetingStatus, QueueBase } from "./models" +export type ComparableEntity = QueueBase | Meeting; export interface ChangeEvent { eventID: number; @@ -21,7 +22,7 @@ const propertyMap: HumanReadableMap = { 'assignee': 'host' } -function detectChanges(versOne: T, versTwo: T, propsToWatch: (keyof T)[]): string | undefined { +function detectChanges(versOne: T, versTwo: T, propsToWatch: (keyof T)[]): string | undefined { for (const property of propsToWatch) { let valueOne = versOne[property] as T[keyof T] | string; let valueTwo = versTwo[property] as T[keyof T] | string; @@ -42,7 +43,7 @@ function detectChanges(versOne: T, versTwo: T, propsToWatch: (ke // https://lodash.com/docs/4.17.15#xorWith -export function compareEntities (oldOnes: T[], newOnes: T[]): string | undefined +export function compareEntities (oldOnes: T[], newOnes: T[]): string | undefined { const symDiff = xorWith(oldOnes, newOnes, isEqual); if (symDiff.length === 0) return; diff --git a/src/assets/src/hooks/useEntityChanges.ts b/src/assets/src/hooks/useEntityChanges.ts index 01be359d..be484714 100644 --- a/src/assets/src/hooks/useEntityChanges.ts +++ b/src/assets/src/hooks/useEntityChanges.ts @@ -1,10 +1,9 @@ import { useState } from "react"; -import { Base } from "../models"; -import { ChangeEvent, compareEntities } from "../changes"; +import { ChangeEvent, compareEntities, ComparableEntity } from "../changes"; -export function useEntityChanges(): +export function useEntityChanges(): [ChangeEvent[], (oldEntities: readonly T[], newEntities: readonly T[]) => void, (key: number) => void] { const [changeEvents, setChangeEvents] = useState([] as ChangeEvent[]); diff --git a/src/assets/src/models.ts b/src/assets/src/models.ts index 2b070786..f7a9b37e 100644 --- a/src/assets/src/models.ts +++ b/src/assets/src/models.ts @@ -10,7 +10,7 @@ export interface MeetingBackend { intl_telephone_url: string | null; } -export interface Base { +interface Base { id: number; } From 2beee7b9579a0011cac4e7eb830a0e1be431ce76 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Mon, 11 Jan 2021 12:04:36 -0500 Subject: [PATCH 18/22] Remove mention of EventMap --- src/assets/src/hooks/useEntityChanges.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/src/hooks/useEntityChanges.ts b/src/assets/src/hooks/useEntityChanges.ts index be484714..986c2b89 100644 --- a/src/assets/src/hooks/useEntityChanges.ts +++ b/src/assets/src/hooks/useEntityChanges.ts @@ -9,7 +9,7 @@ export function useEntityChanges(): const [changeEvents, setChangeEvents] = useState([] as ChangeEvent[]); const [nextID, setNextID] = useState(0); - const compareAndSetEventMap = (oldEntities: readonly T[], newEntities: readonly T[]): void => { + const compareAndSetChangeEvents = (oldEntities: readonly T[], newEntities: readonly T[]): void => { const changeText = compareEntities(oldEntities.slice(), newEntities.slice()); if (changeText !== undefined) { const newChangeEvent = { eventID: nextID, text: changeText } as ChangeEvent; @@ -23,5 +23,5 @@ export function useEntityChanges(): setChangeEvents((prevChangeEvents) => prevChangeEvents.filter((e) => id !== e.eventID)); }; - return [changeEvents, compareAndSetEventMap, deleteChangeEvent]; + return [changeEvents, compareAndSetChangeEvents, deleteChangeEvent]; } From affb03f97372c32eba83fe11c3ad4c3eb7c38c07 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Tue, 19 Jan 2021 16:44:32 -0500 Subject: [PATCH 19/22] Refactor to allow multiple changeMessages, multiple changes; tidy up --- src/assets/src/changes.ts | 136 +++++++++++++++-------- src/assets/src/hooks/useEntityChanges.ts | 19 +++- 2 files changed, 101 insertions(+), 54 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index 459b5c05..fd07caf9 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -1,7 +1,7 @@ import xorWith from "lodash.xorwith"; import isEqual from "lodash.isequal"; -import { isMeeting, isQueueBase, isUser, Meeting, MeetingStatus, QueueBase } from "./models" +import { isMeeting, isQueueBase, isUser, Meeting, MeetingStatus, QueueBase } from "./models"; export type ComparableEntity = QueueBase | Meeting; @@ -13,79 +13,119 @@ export interface ChangeEvent { const queueBasePropsToWatch: (keyof QueueBase)[] = ['status', 'name']; const meetingPropsToWatch: (keyof Meeting)[] = ['backend_type', 'assignee']; -interface HumanReadableMap { - [key: string]: string; + +// Value Transformations + +type ValueTransform = (value: any) => any; + +const transformUserToUsername: ValueTransform = (value) => { + return isUser(value) ? value.username : value +}; + +const transformFalsyToNone: ValueTransform = (value) => { + return value === null || value === undefined || value === '' ? 'None' : value; } -const propertyMap: HumanReadableMap = { +function transformValue (value: any, transforms: ValueTransform[]): any { + let newValue = value; + for (const transform of transforms) { + newValue = transform(newValue); + } + return newValue; +} + +const standardTransforms = [transformUserToUsername, transformFalsyToNone]; + + +// Property Transformations + +interface HumanReadableMap { [key: string]: string; } + +const humanReadablePropertyMap: HumanReadableMap = { 'backend_type': 'meeting type', 'assignee': 'host' } -function detectChanges(versOne: T, versTwo: T, propsToWatch: (keyof T)[]): string | undefined { +const transformProperty = (value: string, propertyMap: HumanReadableMap) => { + return (value in propertyMap) ? propertyMap[value] : value; +} + + +// Core functions + +function detectChanges ( + versOne: T, versTwo: T, propsToWatch: (keyof T)[], transforms: ValueTransform[]): string[] | undefined +{ + let changedPropMessages = []; for (const property of propsToWatch) { let valueOne = versOne[property] as T[keyof T] | string; let valueTwo = versTwo[property] as T[keyof T] | string; - // Check for nested user objects and falsy values - if (isUser(valueOne)) valueOne = valueOne.username; - if (!valueOne) valueOne = 'None'; - if (isUser(valueTwo)) valueTwo = valueTwo.username; - if (!valueTwo) valueTwo = 'None'; + valueOne = transformValue(valueOne, transforms); + valueTwo = transformValue(valueTwo, transforms); if (valueOne !== valueTwo) { - // Make some property strings more human readable - const propName = (property in propertyMap) ? propertyMap[property as string] : property; - return `The ${propName} changed from "${valueOne}" to "${valueTwo}".`; + const propName = transformProperty(property as string, humanReadablePropertyMap); + changedPropMessages.push(`The ${propName} changed from "${valueOne}" to "${valueTwo}".`); } } - return; + if (changedPropMessages.length > 0) return changedPropMessages; +} + + +// Any new types added to ComparableEntity need to be supported in this function. +function describeEntity (entity: ComparableEntity): string[] { + let entityType; + let permIdent; + if (isMeeting(entity)) { + entityType = 'meeting'; + permIdent = `attendee ${entity.attendees[0].username}`; + } else { + // Don't need to check if it's a QueueBase because it's the only other option + entityType = 'queue'; + permIdent = `ID number ${entity.id}`; + } + return [entityType, permIdent]; } // https://lodash.com/docs/4.17.15#xorWith -export function compareEntities (oldOnes: T[], newOnes: T[]): string | undefined +export function compareEntities (oldOnes: T[], newOnes: T[]): string[] | undefined { const symDiff = xorWith(oldOnes, newOnes, isEqual); if (symDiff.length === 0) return; - const firstEntity = symDiff[0]; - const secondEntity = symDiff.length > 1 ? symDiff[1] : undefined; - let entityType; - let permIdentifier; - if (isMeeting(firstEntity)) { - entityType = 'meeting'; - // meeting.attendees may change in the future? - permIdentifier = `attendee ${firstEntity.attendees[0].username}`; - } else if (isQueueBase(firstEntity)) { - entityType = 'queue'; - permIdentifier = `ID number ${firstEntity.id}`; - } else { - console.error(`compareEntities was used with an unsupported type: ${firstEntity}`) - return; - } + const oldIDs = oldOnes.map((value) => value.id); + const newIDs = newOnes.map((value) => value.id); - let message; - if (oldOnes.length < newOnes.length) { - return `A new ${entityType} with ${permIdentifier} was added.`; - } else if (oldOnes.length > newOnes.length) { - return `The ${entityType} with ${permIdentifier} was deleted.`; - } else { - let changeDetected; - if (secondEntity) { + let changeMessages: string[] = []; + let changedIDsProcessed: number[] = []; + for (const entity of symDiff) { + if (changedIDsProcessed.includes(entity.id)) continue; + const [entityType, permIdent] = describeEntity(entity); + if (oldIDs.includes(entity.id) && !newIDs.includes(entity.id)) { + changeMessages.push(`The ${entityType} with ${permIdent} was deleted.`); + } else if (!oldIDs.includes(entity.id) && newIDs.includes(entity.id)) { + changeMessages.push(`A new ${entityType} with ${permIdent} was added.`); + } else { + // Assuming based on context that symDiff.length === 2 + const [firstEntity, secondEntity] = symDiff.filter(value => value.id === entity.id); + let changesDetected: string[] = []; if (isMeeting(firstEntity) && isMeeting(secondEntity)) { - changeDetected = detectChanges(firstEntity, secondEntity, meetingPropsToWatch); - if (!changeDetected && firstEntity.status !== secondEntity.status && secondEntity.status === MeetingStatus.STARTED) { - changeDetected = 'The status indicates the meeting is now in progress.'; + const detectResult = detectChanges(firstEntity, secondEntity, meetingPropsToWatch, standardTransforms); + if (detectResult) changesDetected.push(...detectResult); + // Custom check for Meeting.status, since only some status changes are relevant here. + if (firstEntity.status !== secondEntity.status && secondEntity.status === MeetingStatus.STARTED) { + changesDetected.push('The meeting is now in progress.'); } } else if (isQueueBase(firstEntity) && isQueueBase(secondEntity)) { - changeDetected = detectChanges(firstEntity, secondEntity, queueBasePropsToWatch); + const detectResult = detectChanges(firstEntity, secondEntity, queueBasePropsToWatch, standardTransforms); + if (detectResult) changesDetected.push(...detectResult); } - } - message = `The ${entityType} with ${permIdentifier} was changed.`; - if (changeDetected) { - message = message + ' ' + changeDetected; - return message; + if (changesDetected.length > 0) { + changeMessages.push(`The ${entityType} with ${permIdent} was changed. ` + changesDetected.join(' ')); + } + changedIDsProcessed.push(entity.id) } } - return; + if (changeMessages.length > 0) return changeMessages; } diff --git a/src/assets/src/hooks/useEntityChanges.ts b/src/assets/src/hooks/useEntityChanges.ts index 986c2b89..68c7fe79 100644 --- a/src/assets/src/hooks/useEntityChanges.ts +++ b/src/assets/src/hooks/useEntityChanges.ts @@ -10,11 +10,18 @@ export function useEntityChanges(): const [nextID, setNextID] = useState(0); const compareAndSetChangeEvents = (oldEntities: readonly T[], newEntities: readonly T[]): void => { - const changeText = compareEntities(oldEntities.slice(), newEntities.slice()); - if (changeText !== undefined) { - const newChangeEvent = { eventID: nextID, text: changeText } as ChangeEvent; - setChangeEvents([...changeEvents, newChangeEvent]); - setNextID(nextID + 1); + const changeMessages = compareEntities(oldEntities.slice(), newEntities.slice()); + if (changeMessages !== undefined) { + let eventID = nextID + const newChangeEvents = changeMessages.map( + (m) => { + const newEvent = { eventID: eventID, text: m } as ChangeEvent + eventID++ + return newEvent + } + ) + setChangeEvents([...changeEvents].concat(newChangeEvents)); + setNextID(eventID); } }; @@ -24,4 +31,4 @@ export function useEntityChanges(): }; return [changeEvents, compareAndSetChangeEvents, deleteChangeEvent]; -} +} \ No newline at end of file From c10b9f1b6f8e02d627553817fd11451728d58d9a Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 20 Jan 2021 13:33:19 -0500 Subject: [PATCH 20/22] Add missing semicolons and newline --- src/assets/src/changes.ts | 4 ++-- src/assets/src/hooks/useEntityChanges.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index fd07caf9..f192dab0 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -19,7 +19,7 @@ const meetingPropsToWatch: (keyof Meeting)[] = ['backend_type', 'assignee']; type ValueTransform = (value: any) => any; const transformUserToUsername: ValueTransform = (value) => { - return isUser(value) ? value.username : value + return isUser(value) ? value.username : value; }; const transformFalsyToNone: ValueTransform = (value) => { @@ -44,7 +44,7 @@ interface HumanReadableMap { [key: string]: string; } const humanReadablePropertyMap: HumanReadableMap = { 'backend_type': 'meeting type', 'assignee': 'host' -} +}; const transformProperty = (value: string, propertyMap: HumanReadableMap) => { return (value in propertyMap) ? propertyMap[value] : value; diff --git a/src/assets/src/hooks/useEntityChanges.ts b/src/assets/src/hooks/useEntityChanges.ts index 68c7fe79..198ef7db 100644 --- a/src/assets/src/hooks/useEntityChanges.ts +++ b/src/assets/src/hooks/useEntityChanges.ts @@ -12,12 +12,12 @@ export function useEntityChanges(): const compareAndSetChangeEvents = (oldEntities: readonly T[], newEntities: readonly T[]): void => { const changeMessages = compareEntities(oldEntities.slice(), newEntities.slice()); if (changeMessages !== undefined) { - let eventID = nextID + let eventID = nextID; const newChangeEvents = changeMessages.map( (m) => { - const newEvent = { eventID: eventID, text: m } as ChangeEvent - eventID++ - return newEvent + const newEvent = { eventID: eventID, text: m } as ChangeEvent; + eventID++; + return newEvent; } ) setChangeEvents([...changeEvents].concat(newChangeEvents)); @@ -31,4 +31,4 @@ export function useEntityChanges(): }; return [changeEvents, compareAndSetChangeEvents, deleteChangeEvent]; -} \ No newline at end of file +} From ba5614e6aea1de5ed35bfbece09e6079b0a0959e Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 20 Jan 2021 13:55:47 -0500 Subject: [PATCH 21/22] Simplify return types; rename a var --- src/assets/src/changes.ts | 25 ++++++++++++------------ src/assets/src/hooks/useEntityChanges.ts | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index f192dab0..fdbe9b05 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -54,7 +54,7 @@ const transformProperty = (value: string, propertyMap: HumanReadableMap) => { // Core functions function detectChanges ( - versOne: T, versTwo: T, propsToWatch: (keyof T)[], transforms: ValueTransform[]): string[] | undefined + versOne: T, versTwo: T, propsToWatch: (keyof T)[], transforms: ValueTransform[]): string[] { let changedPropMessages = []; for (const property of propsToWatch) { @@ -67,11 +67,12 @@ function detectChanges ( changedPropMessages.push(`The ${propName} changed from "${valueOne}" to "${valueTwo}".`); } } - if (changedPropMessages.length > 0) return changedPropMessages; + return changedPropMessages; } // Any new types added to ComparableEntity need to be supported in this function. + function describeEntity (entity: ComparableEntity): string[] { let entityType; let permIdent; @@ -89,18 +90,18 @@ function describeEntity (entity: ComparableEntity): string[] { // https://lodash.com/docs/4.17.15#xorWith -export function compareEntities (oldOnes: T[], newOnes: T[]): string[] | undefined +export function compareEntities (oldOnes: T[], newOnes: T[]): string[] { const symDiff = xorWith(oldOnes, newOnes, isEqual); - if (symDiff.length === 0) return; + if (symDiff.length === 0) return []; const oldIDs = oldOnes.map((value) => value.id); const newIDs = newOnes.map((value) => value.id); let changeMessages: string[] = []; - let changedIDsProcessed: number[] = []; + let processedChangedObjectIDs: number[] = []; for (const entity of symDiff) { - if (changedIDsProcessed.includes(entity.id)) continue; + if (processedChangedObjectIDs.includes(entity.id)) continue; const [entityType, permIdent] = describeEntity(entity); if (oldIDs.includes(entity.id) && !newIDs.includes(entity.id)) { changeMessages.push(`The ${entityType} with ${permIdent} was deleted.`); @@ -111,21 +112,21 @@ export function compareEntities (oldOnes: T[], newOn const [firstEntity, secondEntity] = symDiff.filter(value => value.id === entity.id); let changesDetected: string[] = []; if (isMeeting(firstEntity) && isMeeting(secondEntity)) { - const detectResult = detectChanges(firstEntity, secondEntity, meetingPropsToWatch, standardTransforms); - if (detectResult) changesDetected.push(...detectResult); + const changes = detectChanges(firstEntity, secondEntity, meetingPropsToWatch, standardTransforms); + if (changes.length > 0) changesDetected.push(...changes); // Custom check for Meeting.status, since only some status changes are relevant here. if (firstEntity.status !== secondEntity.status && secondEntity.status === MeetingStatus.STARTED) { changesDetected.push('The meeting is now in progress.'); } } else if (isQueueBase(firstEntity) && isQueueBase(secondEntity)) { - const detectResult = detectChanges(firstEntity, secondEntity, queueBasePropsToWatch, standardTransforms); - if (detectResult) changesDetected.push(...detectResult); + const changes = detectChanges(firstEntity, secondEntity, queueBasePropsToWatch, standardTransforms); + if (changes.length > 0) changesDetected.push(...changes); } if (changesDetected.length > 0) { changeMessages.push(`The ${entityType} with ${permIdent} was changed. ` + changesDetected.join(' ')); } - changedIDsProcessed.push(entity.id) + processedChangedObjectIDs.push(entity.id) } } - if (changeMessages.length > 0) return changeMessages; + return changeMessages; } diff --git a/src/assets/src/hooks/useEntityChanges.ts b/src/assets/src/hooks/useEntityChanges.ts index 198ef7db..7e6bdf7f 100644 --- a/src/assets/src/hooks/useEntityChanges.ts +++ b/src/assets/src/hooks/useEntityChanges.ts @@ -11,7 +11,7 @@ export function useEntityChanges(): const compareAndSetChangeEvents = (oldEntities: readonly T[], newEntities: readonly T[]): void => { const changeMessages = compareEntities(oldEntities.slice(), newEntities.slice()); - if (changeMessages !== undefined) { + if (changeMessages.length > 0) { let eventID = nextID; const newChangeEvents = changeMessages.map( (m) => { From f64b6856be61099459fbb5ceab40bf990c37bd87 Mon Sep 17 00:00:00 2001 From: Sam Sciolla Date: Wed, 20 Jan 2021 14:00:18 -0500 Subject: [PATCH 22/22] Remove stray indentation --- src/assets/src/changes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/src/changes.ts b/src/assets/src/changes.ts index fdbe9b05..bb42b64a 100644 --- a/src/assets/src/changes.ts +++ b/src/assets/src/changes.ts @@ -93,7 +93,7 @@ function describeEntity (entity: ComparableEntity): string[] { export function compareEntities (oldOnes: T[], newOnes: T[]): string[] { const symDiff = xorWith(oldOnes, newOnes, isEqual); - if (symDiff.length === 0) return []; + if (symDiff.length === 0) return []; const oldIDs = oldOnes.map((value) => value.id); const newIDs = newOnes.map((value) => value.id);