Skip to content

Commit

Permalink
Add debounced redux subscriber to save redux state to localStorage, &…
Browse files Browse the repository at this point in the history
… load on initialise. Remove use of Set types from redux state
  • Loading branch information
stephenhand committed Dec 12, 2023
1 parent 9be4f47 commit 3caf337
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 99 deletions.
4 changes: 3 additions & 1 deletion plugin-hrm-form/src/HrmFormPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ import { setUpReferrableResources } from './components/resources/setUpReferrable
import { subscribeNewMessageAlertOnPluginInit } from './notifications/newMessage';
import { subscribeReservedTaskAlert } from './notifications/reservedTask';
import { setUpCounselorToolkits } from './components/toolkits/setUpCounselorToolkits';
import { setupConferenceComponents, setUpConferenceActions } from './conference';
import { setUpConferenceActions, setupConferenceComponents } from './conference';
import { setUpTransferActions } from './transfer/setUpTransferActions';
import { playNotification } from './notifications/playNotification';
import { namespace } from './states/storeNamespaces';
import { activateStatePersistence } from './states/persistState';

const PLUGIN_NAME = 'HrmFormPlugin';

Expand Down Expand Up @@ -245,6 +246,7 @@ export default class HrmFormPlugin extends FlexPlugin {
* This is a workaround until we deprecate 'getConfig' in it's current form after we migrate to Flex 2.0
*/
subscribeToConfigUpdates(manager);
activateStatePersistence();
}
}

Expand Down

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions plugin-hrm-form/src/states/contacts/contactState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { ITask, TaskHelper } from '@twilio/flex-ui';
import type { ContactMetadata } from './types';
import { ReferralLookupStatus } from './resourceReferral';
import type { ContactState } from './existingContacts';
import { Contact, ContactRawJson, OfflineContactTask, isOfflineContactTask } from '../../types/types';
import { Contact, ContactRawJson, isOfflineContactTask, OfflineContactTask } from '../../types/types';
import { createStateItem, getInitialValue } from '../../components/common/forms/formGenerators';
import { createContactlessTaskTabDefinition } from '../../components/tabbedForms/ContactlessTaskTabDefinition';
import { getHrmConfig } from '../../hrmConfig';
Expand Down Expand Up @@ -111,5 +111,5 @@ export const newContactState = (definitions: DefinitionVersion, task?: ITask | O
savedContact: newContact(definitions, task),
metadata: newContactMetaData(recreated),
draftContact: {},
references: new Set(),
references: {},
});
17 changes: 8 additions & 9 deletions plugin-hrm-form/src/states/contacts/existingContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ConfigurationState } from '../configuration/reducer';
import { transformValuesForContactForm } from './contactDetailsAdapter';
import { ContactMetadata } from './types';
import { newContactMetaData } from './contactState';
import { add, has, remove, SerializableSet, size } from '../serializableSet';

export enum ContactDetailsRoute {
EDIT_CALLER_INFORMATION = 'editCallerInformation',
Expand Down Expand Up @@ -85,7 +86,7 @@ export type TranscriptResult = {
};

export type ContactState = {
references: Set<string>;
references: SerializableSet;
savedContact: Contact;
draftContact?: ContactDraftChanges;
metadata: ContactMetadata;
Expand Down Expand Up @@ -130,12 +131,10 @@ export const refreshContact = (contact: any) => loadContact(contact, undefined,
export const loadContactReducer = (state = initialState, action: LoadContactAction) => {
const updateEntries = action.contacts
.filter(c => {
return (
(action.reference && !(state[c.id]?.references ?? new Set()).has(action.reference)) || action.replaceExisting
);
return (action.reference && !has(state[c.id]?.references ?? {}, action.reference)) || action.replaceExisting;
})
.map(c => {
const current = state[c.id] ?? { references: new Set() };
const current = state[c.id] ?? { references: {} };
const { draftContact, ...currentContact } = state[c.id] ?? {
categories: {
expanded: {},
Expand All @@ -147,8 +146,8 @@ export const loadContactReducer = (state = initialState, action: LoadContactActi
{
metadata: newContactMetaData(true),
...currentContact,
savedContact: action.replaceExisting || !current.references.size ? c : state[c.id].savedContact,
references: action.reference ? current.references.add(action.reference) : current.references,
savedContact: action.replaceExisting || !size(current.references) ? c : state[c.id].savedContact,
references: action.reference ? add(current.references, action.reference) : current.references,
draftContact: action.replaceExisting ? undefined : draftContact,
},
];
Expand Down Expand Up @@ -189,10 +188,10 @@ export const releaseContactReducer = (state: ExistingContactsState, action: Rele
);
return [id, undefined];
}
current.references.delete(action.reference);
remove(current.references, action.reference);
return [id, current];
})
.filter(([, ecs]) => typeof ecs === 'object' && ecs.references.size > 0);
.filter(([, ecs]) => typeof ecs === 'object' && size(ecs.references) > 0);
return {
...omit(state, ...action.ids),
...Object.fromEntries(updateKvps),
Expand Down
6 changes: 3 additions & 3 deletions plugin-hrm-form/src/states/contacts/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
ContactsState,
CREATE_CONTACT_ACTION,
LOAD_CONTACT_FROM_HRM_BY_TASK_ID_ACTION,
SET_SAVED_CONTACT, UPDATE_CONTACT_ACTION,
SET_SAVED_CONTACT,
UPDATE_CONTACT_ACTION,
} from './types';
import { REMOVE_CONTACT_STATE, RemoveContactStateAction } from '../types';
import {
Expand All @@ -34,7 +35,6 @@ import {
EXISTING_CONTACT_TOGGLE_CATEGORY_EXPANDED_ACTION,
EXISTING_CONTACT_UPDATE_DRAFT_ACTION,
ExistingContactAction,
initialState as existingContactInitialState,
LOAD_CONTACT_ACTION,
loadContactReducer,
loadTranscriptReducer,
Expand Down Expand Up @@ -66,7 +66,7 @@ export const emptyCategories = [];
// exposed for testing
export const initialState: ContactsState = {
existingContacts: {},
contactsBeingCreated: new Set<string>(),
contactsBeingCreated: {},
contactDetails: {
[DetailsContext.CASE_DETAILS]: { detailsExpanded: {} },
[DetailsContext.CONTACT_SEARCH]: { detailsExpanded: {} },
Expand Down
26 changes: 13 additions & 13 deletions plugin-hrm-form/src/states/contacts/saveContact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,29 @@ import { format } from 'date-fns';
import { submitContactForm } from '../../services/formSubmissionHelpers';
import {
connectToCase,
removeFromCase,
createContact,
getContactById,
getContactByTaskSid,
removeFromCase,
updateContactInHrm,
} from '../../services/ContactService';
import { Case, CustomITask, Contact } from '../../types/types';
import { Case, Contact, CustomITask } from '../../types/types';
import {
CONNECT_TO_CASE,
REMOVE_FROM_CASE,
ContactMetadata,
ContactsState,
CREATE_CONTACT_ACTION,
LOAD_CONTACT_FROM_HRM_BY_ID_ACTION,
LOAD_CONTACT_FROM_HRM_BY_TASK_ID_ACTION,
REMOVE_FROM_CASE,
SET_SAVED_CONTACT,
UPDATE_CONTACT_ACTION,
} from './types';
import { ContactDraftChanges } from './existingContacts';
import { newContactMetaData } from './contactState';
import { cancelCase, getCase } from '../../services/CaseService';
import { getCase } from '../../services/CaseService';
import { getUnsavedContact } from './getUnsavedContact';
import { add, remove } from '../serializableSet';

export const createContactAsyncAction = createAsyncAction(
CREATE_CONTACT_ACTION,
Expand Down Expand Up @@ -197,13 +198,13 @@ const loadContactIntoRedux = (
newMetadata?: ContactMetadata,
): ContactsState => {
const { existingContacts } = state;
const references = existingContacts[contact.id]?.references ?? new Set();
const references = existingContacts[contact.id]?.references ?? {};
if (reference) {
references.add(reference);
add(references, reference);
}
const metadata = newMetadata ?? existingContacts[contact.id]?.metadata;
const contactsBeingCreated = new Set(state.contactsBeingCreated);
contactsBeingCreated.delete(contact.taskId);
const contactsBeingCreated = { ...state.contactsBeingCreated };
remove(contactsBeingCreated, contact.taskId);
return {
...state,
contactsBeingCreated,
Expand Down Expand Up @@ -288,11 +289,10 @@ export const saveContactReducer = (initialState: ContactsState) =>
handleAction(
createContactAsyncAction.pending as typeof createContactAsyncAction,
(state, { meta: { taskSid } }): ContactsState => {
const contactsBeingCreated = new Set(state.contactsBeingCreated);
contactsBeingCreated.add(taskSid);
const contactsBeingCreated = { ...state.contactsBeingCreated };
return {
...state,
contactsBeingCreated,
contactsBeingCreated: add(contactsBeingCreated, taskSid),
};
},
),
Expand All @@ -310,8 +310,8 @@ export const saveContactReducer = (initialState: ContactsState) =>
} = action as typeof action & {
meta: { taskSid: string };
};
const contactsBeingCreated = new Set(state.contactsBeingCreated);
contactsBeingCreated.delete(taskSid);
const contactsBeingCreated = { ...state.contactsBeingCreated };
remove(contactsBeingCreated, taskSid);
return {
...state,
contactsBeingCreated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { RootState } from '..';
import { namespace } from '../storeNamespaces';
import { has, size } from '../serializableSet';

export const selectIsContactCreating = (
{
Expand All @@ -24,12 +25,12 @@ export const selectIsContactCreating = (
},
}: RootState,
taskSid: string,
) => contactsBeingCreated.has(taskSid);
) => has(contactsBeingCreated, taskSid);

export const selectAnyContactIsSaving = ({
[namespace]: {
activeContacts: { contactsBeingCreated, existingContacts },
},
}: RootState) =>
contactsBeingCreated.size > 0 ||
size(contactsBeingCreated) > 0 ||
Object.values(existingContacts).some(({ metadata }) => metadata?.saveStatus === 'saving');
3 changes: 2 additions & 1 deletion plugin-hrm-form/src/states/contacts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Case, Contact } from '../../types/types';
import { DraftResourceReferralState } from './resourceReferral';
import { ContactState, ExistingContactsState } from './existingContacts';
import { ContactDetailsState } from './contactDetails';
import { SerializableSet } from '../serializableSet';

// Action types
export const SAVE_END_MILLIS = 'SAVE_END_MILLIS';
Expand Down Expand Up @@ -63,7 +64,7 @@ export type ContactMetadata = {

export type ContactsState = {
existingContacts: ExistingContactsState;
contactsBeingCreated: Set<string>;
contactsBeingCreated: SerializableSet;
contactDetails: ContactDetailsState;
isCallTypeCaller: boolean;
};
Expand Down
8 changes: 5 additions & 3 deletions plugin-hrm-form/src/states/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { CaseState } from './case/types';
import { ContactsState } from './contacts/types';
import {
caseListBase,
caseMergingBannersBase,
conferencingBase,
configurationBase,
connectedCaseBase,
Expand All @@ -42,14 +43,14 @@ import {
csamReportBase,
dualWriteBase,
namespace,
profileBase,
queuesStatusBase,
referrableResourcesBase,
routingBase,
searchContactsBase,
caseMergingBannersBase,
profileBase,
} from './storeNamespaces';
import { reduce as CaseMergingBannersReducer } from './case/caseBanners';
import { readPersistedState } from './persistState';

const reducers = {
[searchContactsBase]: SearchFormReducer,
Expand Down Expand Up @@ -78,7 +79,8 @@ export type RootState = FlexState & { [namespace]: HrmState };
const combinedReducers = combineReducers(reducers);

// Combine the reducers
const reducer = (state: HrmState, action): HrmState => {
const reducer = (currentState: HrmState, action): HrmState => {
const state = currentState ?? readPersistedState();
return {
...combinedReducers(state, action),
/*
Expand Down
46 changes: 46 additions & 0 deletions plugin-hrm-form/src/states/persistState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import { Manager } from '@twilio/flex-ui';
import _ from 'lodash';

import { RootState } from '.';
import { namespace } from './storeNamespaces';
import { getAseloFeatureFlags } from '../hrmConfig';

// Quick & dirty module to persist redux state to localStorage via subscriptions since we can't add middleware like redux-persist to do it for us
export const activateStatePersistence = () => {
if (getAseloFeatureFlags().enable_local_redux_persist) {
const debouncedWrite = _.debounce(() => {
// Exclude configuration from persisted state, since it contains non serializable elements, and is read only in the client anyway
const {
[namespace]: { configuration, ...persistableState },
} = Manager.getInstance().store.getState() as RootState;
localStorage.setItem('redux-state/plugin-hrm-form', JSON.stringify(persistableState));
}, 1000);
Manager.getInstance().store.subscribe(debouncedWrite);
}
};

export const readPersistedState = (): RootState[typeof namespace] | null => {
if (getAseloFeatureFlags().enable_local_redux_persist) {
const persistedState = localStorage.getItem('redux-state/plugin-hrm-form');
if (persistedState) {
return JSON.parse(persistedState);
}
}
return undefined;
};
36 changes: 36 additions & 0 deletions plugin-hrm-form/src/states/serializableSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

// The JS set doesn't serialize to JSON, so we need to use a plain object instead if we want to keep it in redux state, now we store it in localStorage

export type SerializableSet = Record<string, true>;

export const has = (set: SerializableSet, key: string): boolean => Boolean(set[key]);

export const add = (set: SerializableSet, key: string): SerializableSet => {
set[key] = true;
return set;
};

export const remove = (set: SerializableSet, key: string): boolean => {
if (!has(set, key)) {
return false;
}
delete set[key];
return true;
};

export const size = (set: SerializableSet): number => Object.keys(set).length;
1 change: 1 addition & 0 deletions plugin-hrm-form/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export type FeatureFlags = {
enable_client_profiles: boolean; // Enables Client Profiles
enable_case_merging: boolean; // Enables adding contacts to existing cases
enable_confirm_on_browser_close: boolean; // Enables confirmation dialog on browser close when there are unsaved changes
enable_local_redux_persist: boolean; // Enables storing redux state in localStorage
};
/* eslint-enable camelcase */

Expand Down
4 changes: 2 additions & 2 deletions plugin-hrm-form/src/utils/sharedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ITask, Manager } from '@twilio/flex-ui';
import { recordBackendError } from '../fullStory';
import { issueSyncToken } from '../services/ServerlessService';
import { getAseloFeatureFlags, getDefinitionVersions, getHrmConfig, getTemplateStrings } from '../hrmConfig';
import { CSAMReportEntry, Contact } from '../types/types';
import { Contact, CSAMReportEntry } from '../types/types';
import { ContactMetadata } from '../states/contacts/types';
import { ChannelTypes } from '../states/DomainConstants';
import { ResourceReferral } from '../states/contacts/resourceReferral';
Expand Down Expand Up @@ -89,7 +89,7 @@ const transferFormToContactState = (transferForm: TransferForm, baselineContact:
...metadata,
draft: form.draft,
},
references: new Set<string>(),
references: {},
};
};

Expand Down

0 comments on commit 3caf337

Please sign in to comment.