diff --git a/src/abstract/lib/types/entityProgress.ts b/src/abstract/lib/types/entityProgress.ts index 5f53452e1..f7b86fac5 100644 --- a/src/abstract/lib/types/entityProgress.ts +++ b/src/abstract/lib/types/entityProgress.ts @@ -4,10 +4,6 @@ export const enum ActivityPipelineType { Flow, } -export type EntityName = { - entityName: string; -}; - export type FlowProgress = { type: ActivityPipelineType.Flow; pipelineActivityOrder: number; @@ -18,11 +14,13 @@ export type FlowProgress = { currentActivityImage: string | null; currentActivityStartAt: number | null; executionGroupKey: string; -} & EntityName; + entityName: string; +}; export type ActivityProgress = { type: ActivityPipelineType.Regular; -} & EntityName; + entityName: string; +}; export type EntityProgress = FlowProgress | ActivityProgress; diff --git a/src/app/model/migrations/MigrationFactory.ts b/src/app/model/migrations/MigrationFactory.ts index 74c37e869..b54b92e19 100644 --- a/src/app/model/migrations/MigrationFactory.ts +++ b/src/app/model/migrations/MigrationFactory.ts @@ -1,3 +1,4 @@ +import { MigrationToVersion0002 } from '@app/app/model/migrations/migrations/to0002/MigrationToVersion0002'; import { __queryClient__ } from '@app/app/ui/AppProvider/ReactQueryProvider'; import { MigrationToVersion0001 } from './migrations/to0001/MigrationToVersion0001'; @@ -11,6 +12,7 @@ export class MigrationFactory { public createMigrations(): Migrations { return { 1: new MigrationToVersion0001(__queryClient__), + 2: new MigrationToVersion0002(__queryClient__), }; } } diff --git a/src/app/model/migrations/migrations/to0002/MigrationDtoTypes0002.ts b/src/app/model/migrations/migrations/to0002/MigrationDtoTypes0002.ts new file mode 100644 index 000000000..9ff91d781 --- /dev/null +++ b/src/app/model/migrations/migrations/to0002/MigrationDtoTypes0002.ts @@ -0,0 +1,28 @@ +import { ImageUrl } from '@app/shared/lib'; + +export type ActivityRecordDto = { + id: string; + name: string; + description: string; + image: ImageUrl | null; + order: number; +}; + +export type ActivityFlowRecordDto = { + id: string; + name: string; + description: string; + order: number; + activityIds: Array; +}; + +export type AppletDetailsDto = { + id: string; + displayName: string; + activities: ActivityRecordDto[]; + activityFlows: ActivityFlowRecordDto[]; +}; + +export type AppletDetailsResponse = { + result: AppletDetailsDto; +}; diff --git a/src/app/model/migrations/migrations/to0002/MigrationReduxTypes0002.ts b/src/app/model/migrations/migrations/to0002/MigrationReduxTypes0002.ts new file mode 100644 index 000000000..fd3eec0e4 --- /dev/null +++ b/src/app/model/migrations/migrations/to0002/MigrationReduxTypes0002.ts @@ -0,0 +1,81 @@ +import { ActivityPipelineType } from '@app/abstract/lib'; + +export type FlowProgressFrom = { + type: ActivityPipelineType.Flow; + pipelineActivityOrder: number; + totalActivitiesInPipeline: number; + currentActivityId: string; + currentActivityName: string; + currentActivityDescription: string; + currentActivityImage: string | null; + currentActivityStartAt: number | null; + executionGroupKey: string; +}; + +type ActivityProgressFrom = { + type: ActivityPipelineType.Regular; +}; + +type EntityProgressFrom = FlowProgressFrom | ActivityProgressFrom; + +export type StoreProgressPayloadFrom = EntityProgressFrom & { + startAt: number; + endAt: number | null; +}; + +type StoreEventsProgressFrom = Record; + +type StoreEntitiesProgressFrom = Record; + +type StoreProgressFrom = Record; + +export type FlowProgressTo = { + type: ActivityPipelineType.Flow; + pipelineActivityOrder: number; + totalActivitiesInPipeline: number; + currentActivityId: string; + currentActivityName: string; + currentActivityDescription: string; + currentActivityImage: string | null; + currentActivityStartAt: number | null; + executionGroupKey: string; + entityName: string; +}; + +type ActivityProgressTo = { + type: ActivityPipelineType.Regular; + entityName: string; +}; + +type EntityProgressTo = FlowProgressTo | ActivityProgressTo; + +export type StoreProgressPayloadTo = EntityProgressTo & { + startAt: number; + endAt: number | null; +}; + +type StoreEventsProgressTo = Record; + +type StoreEntitiesProgressTo = Record; + +export type StoreProgressTo = Record; + +export type RootStateFrom = { + applets: { + inProgress: StoreProgressFrom; + completedEntities: any; + completions: any; + }; + streaming: any; + identity: any; +}; + +export type RootStateTo = { + applets: { + inProgress: StoreProgressTo; + completedEntities: any; + completions: any; + }; + streaming: any; + identity: any; +}; diff --git a/src/app/model/migrations/migrations/to0002/MigrationStorageTypes0002.ts b/src/app/model/migrations/migrations/to0002/MigrationStorageTypes0002.ts new file mode 100644 index 000000000..e5364a5d7 --- /dev/null +++ b/src/app/model/migrations/migrations/to0002/MigrationStorageTypes0002.ts @@ -0,0 +1,21 @@ +type FlowPipelineItemFrom = { + type: 'Stepper' | 'Intermediate' | 'Summary' | 'Finish'; + payload: { + appletId: string; + activityId: string; + eventId: string; + flowId?: string; + order: number; + activityName: string; + activityDescription?: string; + activityImage?: string | null; + }; +}; + +export type FlowStateFrom = { + step: number; + pipeline: FlowPipelineItemFrom[]; + isCompletedDueToTimer: boolean; + context: Record; + flowName: string; +}; diff --git a/src/app/model/migrations/migrations/to0002/MigrationToVersion0002.ts b/src/app/model/migrations/migrations/to0002/MigrationToVersion0002.ts new file mode 100644 index 000000000..2cd9f57ad --- /dev/null +++ b/src/app/model/migrations/migrations/to0002/MigrationToVersion0002.ts @@ -0,0 +1,212 @@ +import { QueryClient } from '@tanstack/react-query'; + +import { Logger } from '@app/shared/lib'; + +import { + RootStateFrom, + RootStateTo, + StoreProgressPayloadTo, +} from './MigrationReduxTypes0002.ts'; +import { FlowStateFrom } from './MigrationStorageTypes0002.ts'; +import { + getUpdatedReduxState, + selectNotCompletedActivities, + selectNotCompletedFlows, +} from './MigrationUtils0002'; +import { QueryDataUtils } from './MigrationUtils0002.ts'; +import { + NotCompletedEntitiesFrom, + NotCompletedEntitiesTo, +} from './MigrationUtils0002.ts'; +import { + IMigration, + MigrationInput, + MigrationOutput, + Storages, +} from '../../types'; +import { getStorageRecord } from '../../utils.ts'; + +export class MigrationToVersion0002 implements IMigration { + private queryDataUtils: QueryDataUtils; + + constructor(queryClient: QueryClient) { + this.queryDataUtils = new QueryDataUtils(queryClient); + } + + private getFlowState = (key: string): FlowStateFrom | null => { + return getStorageRecord(Storages.FlowProgress, key); + }; + + private getFlowRecordKey = ( + appletId: string, + flowId: string | null, + eventId: string, + ) => { + const flowKey = flowId ?? 'default_one_step_flow'; + return `${flowKey}-${appletId}-${eventId}`; + }; + + private getUpdatedFlowProgress( + progressFlowFrom: NotCompletedEntitiesFrom, + entityName: string, + ): NotCompletedEntitiesTo { + const storeProgressPayloadTo: StoreProgressPayloadTo = { + ...progressFlowFrom.payload, + entityName: entityName, + }; + + return { + appletId: progressFlowFrom.appletId, + eventId: progressFlowFrom.eventId, + entityId: progressFlowFrom.entityId, + type: progressFlowFrom.type, + payload: storeProgressPayloadTo, + }; + } + + private getUpdatedActivityProgress( + progressFlowFrom: NotCompletedEntitiesFrom, + entityName: string, + ): NotCompletedEntitiesTo { + const storeProgressPayloadTo: StoreProgressPayloadTo = { + ...progressFlowFrom.payload, + entityName: entityName, + }; + + return { + appletId: progressFlowFrom.appletId, + eventId: progressFlowFrom.eventId, + entityId: progressFlowFrom.entityId, + type: progressFlowFrom.type, + payload: storeProgressPayloadTo, + }; + } + private getUpdatedProgressForFlows( + reduxState: RootStateFrom, + ): NotCompletedEntitiesTo[] { + const progressFlowsFrom = selectNotCompletedFlows(reduxState); + const progressFlowsTo: NotCompletedEntitiesTo[] = []; + + for (const progressFlowFrom of progressFlowsFrom) { + const { appletId, entityId, eventId } = progressFlowFrom; + + const logAppletName = '', + logFlowName = ''; + const logFlowStateFrom = ''; + const logCurrentActivityDto = ''; + const logActivityFlowDto = ''; + const logProgressFlowFrom = JSON.stringify(progressFlowFrom, null, 2); + + try { + const key = this.getFlowRecordKey(appletId, entityId, eventId); + + const flowStateFrom = this.getFlowState(key)!; + + if (!flowStateFrom) { + Logger.warn( + `[MigrationToVersion0002.getUpdatedProgressForFlows]: Migration cannot be executed as flowState doesn't exist appletName=${logAppletName}, appletId=${appletId}, entityId=${entityId}, eventId=${eventId}`, + ); + continue; + } + const flowName = flowStateFrom.flowName; + + const progressFlowTo = this.getUpdatedFlowProgress( + progressFlowFrom, + flowName, + ); + + progressFlowsTo.push(progressFlowTo); + } catch (error) { + Logger.warn( + `[MigrationToVersion0002.getUpdatedProgressForFlows]: Error occurred, appletName=${logAppletName}, flowName=${logFlowName}, progressFlowFrom=${logProgressFlowFrom}, flowStateFrom=${logFlowStateFrom}, currentActivityDto=${logCurrentActivityDto}, activityFlowDto=${logActivityFlowDto} \nerror: \n${error}`, + ); + } + } + + return progressFlowsTo; + } + + private getUpdatedProgressForActivities( + reduxState: RootStateFrom, + ): NotCompletedEntitiesTo[] { + const progressActivitiesFrom = selectNotCompletedActivities(reduxState); + const progressActivitiesTo = []; + + for (const progressActivityFrom of progressActivitiesFrom) { + const { appletId, entityId } = progressActivityFrom; + let logAppletName = '', + logActivityName = ''; + const logActivityStateFrom = ''; + const logCurrentActivityDto = ''; + const logActivityActivityDto = ''; + const logProgressActivityFrom = JSON.stringify( + progressActivityFrom, + null, + 2, + ); + + try { + const appletDto = this.queryDataUtils.getAppletDto(appletId); + + if (!appletDto) { + Logger.warn( + "[MigrationToVersion0002.getUpdatedProgressForActivities]: Migration cannot be executed as applet doesn't exist: " + + appletId, + ); + continue; + } + logAppletName = appletDto.displayName; + + const activityDto = appletDto.activities.find(f => f.id === entityId); + + if (!activityDto) { + Logger.warn( + "[MigrationToVersion0002.getUpdatedProgressForActivities]: activityFlow doesn't exist: " + + entityId, + ); + continue; + } + + logActivityName = activityDto.name; + + const progressActivityTo = this.getUpdatedActivityProgress( + progressActivityFrom, + activityDto.name, + ); + + progressActivitiesTo.push(progressActivityTo); + } catch (error) { + Logger.warn( + `[MigrationToVersion0002.getUpdatedProgressForActivities]: Error occurred, appletName=${logAppletName}, flowName=${logActivityName}, progressActivityFrom=${logProgressActivityFrom}, flowStateFrom=${logActivityStateFrom}, currentActivityDto=${logCurrentActivityDto}, activityActivityDto=${logActivityActivityDto} \nerror: \n${error}`, + ); + } + } + + return progressActivitiesTo; + } + + private getUpdatedProgress( + reduxState: RootStateFrom, + ): NotCompletedEntitiesTo[] { + const activitiesProgressTo = + this.getUpdatedProgressForActivities(reduxState); + const flowsProgressTo = this.getUpdatedProgressForFlows(reduxState); + + return [...activitiesProgressTo, ...flowsProgressTo]; + } + + migrate(input: MigrationInput): MigrationOutput { + const result: MigrationOutput = { + reduxState: { ...input.reduxState } as RootStateTo, + }; + + const reduxRootStateFrom: RootStateFrom = input.reduxState as RootStateFrom; + + const progressTo = this.getUpdatedProgress(reduxRootStateFrom); + result.reduxState = getUpdatedReduxState(reduxRootStateFrom, progressTo); + + return result; + } +} + +export default MigrationToVersion0002; diff --git a/src/app/model/migrations/migrations/to0002/MigrationUtils0002.ts b/src/app/model/migrations/migrations/to0002/MigrationUtils0002.ts new file mode 100644 index 000000000..d2ca3137c --- /dev/null +++ b/src/app/model/migrations/migrations/to0002/MigrationUtils0002.ts @@ -0,0 +1,173 @@ +import { QueryClient } from '@tanstack/react-query'; + +import { ActivityPipelineType } from '@app/abstract/lib'; +import { getAppletDetailsKey, getDataFromQuery } from '@app/shared/lib'; + +import { + AppletDetailsDto, + AppletDetailsResponse, +} from './MigrationDtoTypes0002'; +import { + RootStateFrom, + RootStateTo, + StoreProgressPayloadFrom, + StoreProgressPayloadTo, + StoreProgressTo, +} from './MigrationReduxTypes0002'; + +export type NotCompletedEntitiesTo = { + type: ActivityPipelineType; + appletId: string; + entityId: string; + eventId: string; + payload: StoreProgressPayloadTo; +}; + +export type NotCompletedEntitiesFrom = { + type: ActivityPipelineType; + appletId: string; + entityId: string; + eventId: string; + payload: StoreProgressPayloadFrom; +}; + +export const selectNotCompletedFlows = ( + state: RootStateFrom, +): NotCompletedEntitiesFrom[] => { + const inProgressApplets = state.applets.inProgress ?? {}; + const result: NotCompletedEntitiesFrom[] = []; + + const appletIds = Object.keys(inProgressApplets); + + for (const appletId of appletIds) { + const progressEntities = inProgressApplets[appletId] ?? {}; + + const entityIds = Object.keys(progressEntities); + + for (const entityId of entityIds) { + const progressEvents = progressEntities[entityId] ?? {}; + + const eventIds = Object.keys(progressEvents); + + for (const eventId of eventIds) { + const payload = progressEvents[eventId] ?? {}; + + if ( + !progressEvents[eventId] || + payload.endAt || + payload.type === ActivityPipelineType.Regular + ) { + continue; + } + + result.push({ + appletId, + entityId, + eventId, + type: payload.type, + payload, + }); + } + } + } + + return result; +}; + +export const selectNotCompletedActivities = ( + state: RootStateFrom, +): NotCompletedEntitiesFrom[] => { + const inProgressApplets = state.applets.inProgress ?? {}; + const result: NotCompletedEntitiesFrom[] = []; + + const appletIds = Object.keys(inProgressApplets); + + for (const appletId of appletIds) { + const progressEntities = inProgressApplets[appletId] ?? {}; + const entityIds = Object.keys(progressEntities); + + for (const entityId of entityIds) { + const progressEvents = progressEntities[entityId] ?? {}; + + const eventIds = Object.keys(progressEvents); + + for (const eventId of eventIds) { + const payload = progressEvents[eventId] ?? {}; + + if ( + !progressEvents[eventId] || + payload.endAt || + payload.type !== ActivityPipelineType.Regular + ) { + continue; + } + + result.push({ + appletId, + entityId, + eventId, + type: payload.type, + payload, + }); + } + } + } + + return result; +}; + +export class QueryDataUtils { + private queryClient: QueryClient; + + constructor(queryClient: QueryClient) { + this.queryClient = queryClient; + } + + getAppletDto(appletId: string): AppletDetailsDto | null { + const result = getDataFromQuery( + getAppletDetailsKey(appletId), + this.queryClient, + ); + + return result?.result ?? null; + } +} + +export const getUpdatedReduxState = ( + stateFrom: RootStateFrom, + progressFlowsTo: NotCompletedEntitiesTo[], +): RootStateTo => { + let result: RootStateTo = { + ...stateFrom, + applets: { + ...stateFrom.applets, + inProgress: { ...stateFrom.applets.inProgress } as StoreProgressTo, + }, + }; + + for (const entity of progressFlowsTo) { + result = { + ...result, + applets: { + ...result.applets, + inProgress: { + ...result.applets.inProgress, + [entity.appletId]: { + ...result.applets.inProgress[entity.appletId], + [entity.entityId]: { + ...result.applets.inProgress[entity.appletId][entity.entityId], + [entity.eventId]: { + ...result.applets.inProgress[entity.appletId][entity.entityId][ + entity.eventId + ], + ...entity.payload, + }, + }, + }, + }, + }, + }; + } + + return result; +}; diff --git a/src/app/model/migrations/types.ts b/src/app/model/migrations/types.ts index 460fe5785..acb7c3406 100644 --- a/src/app/model/migrations/types.ts +++ b/src/app/model/migrations/types.ts @@ -6,10 +6,17 @@ import { FlowStateFrom as FlowState0000, FlowStateTo as FlowState0001, } from './migrations/to0001/MigrationStorageTypes0001'; - +import { + RootStateTo as RootStateTo0002, + RootStateFrom as RootStateFrom0002, +} from './migrations/to0002/MigrationReduxTypes0002.ts'; type FlowState = FlowState0000 | FlowState0001; -export type ReduxRootState = RootState0000 | RootState0001; +export type ReduxRootState = + | RootState0000 + | RootState0001 + | RootStateTo0002 + | RootStateFrom0002; export type MigrationInput = { reduxState: ReduxRootState; diff --git a/src/entities/activity/lib/types/activityListItem.ts b/src/entities/activity/lib/types/activityListItem.ts index f5ed01e27..a124b34d4 100644 --- a/src/entities/activity/lib/types/activityListItem.ts +++ b/src/entities/activity/lib/types/activityListItem.ts @@ -14,7 +14,7 @@ export type ActivityListItem = { isInActivityFlow: boolean; - activityFlowDetails?: { + activityFlowDetails: { showActivityFlowBadge: boolean; activityFlowName: string; numberOfActivitiesInFlow: number; diff --git a/src/entities/applet/model/slice.ts b/src/entities/applet/model/slice.ts index 66fb5dedd..f9a875394 100644 --- a/src/entities/applet/model/slice.ts +++ b/src/entities/applet/model/slice.ts @@ -41,7 +41,7 @@ type InProgressFlow = { eventId: string; pipelineActivityOrder: number; availableTo?: Date | null; - flowName: string; + flowName?: string; }; type InitialState = { @@ -106,7 +106,7 @@ const slice = createSlice({ executionGroupKey: uuidv4(), pipelineActivityOrder, totalActivitiesInPipeline: totalActivities, - entityName: flowName, + entityName: flowName!, }; state.inProgress[appletId] = state.inProgress[appletId] ?? {}; @@ -127,7 +127,6 @@ const slice = createSlice({ eventId, pipelineActivityOrder, totalActivities, - flowName, } = action.payload; const event = state.inProgress[appletId][flowId][eventId] as FlowProgress; @@ -139,7 +138,6 @@ const slice = createSlice({ event.pipelineActivityOrder = pipelineActivityOrder; event.currentActivityStartAt = new Date().getTime(); event.totalActivitiesInPipeline = totalActivities; - event.entityName = flowName; }, entityCompleted: ( diff --git a/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts b/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts index 8021e0649..b99d253f4 100644 --- a/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts +++ b/src/widgets/activity-group/model/factories/ActivityGroupsBuilder.test.ts @@ -119,6 +119,7 @@ const getExpectedItem = (): ActivityListItem => { isInActivityFlow: false, image: undefined, availableTo: null, + activityFlowDetails: null, }; return expectedItem; }; diff --git a/src/widgets/activity-group/model/factories/ListItemsFactory.ts b/src/widgets/activity-group/model/factories/ListItemsFactory.ts index 1a71d714c..4e7573eda 100644 --- a/src/widgets/activity-group/model/factories/ListItemsFactory.ts +++ b/src/widgets/activity-group/model/factories/ListItemsFactory.ts @@ -1,5 +1,6 @@ import { ActivityPipelineType, + ActivityProgress, AvailabilityType, FlowProgress, } from '@app/abstract/lib'; @@ -56,8 +57,7 @@ export class ListItemsFactory { progressRecord.pipelineActivityOrder + 1; item.activityFlowDetails.numberOfActivitiesInFlow = progressRecord.totalActivitiesInPipeline; - item.activityFlowDetails.activityFlowName = - progressRecord.entityName || activityFlow.name; + item.activityFlowDetails.activityFlowName = progressRecord.entityName; } else { activity = this.utility.activities.find( x => x.id === activityFlow.activityIds[0], @@ -70,6 +70,7 @@ export class ListItemsFactory { item.activityFlowDetails.activityPositionInFlow = 1; item.activityFlowDetails.numberOfActivitiesInFlow = activityFlow.activityIds.length; + item.activityFlowDetails.activityFlowName = activityFlow.name; } } @@ -82,9 +83,9 @@ export class ListItemsFactory { if (isInProgress) { const progressRecord = this.utility.getProgressRecord( activityEvent, - ) as FlowProgress; + ) as ActivityProgress; - item.name = progressRecord.entityName || item.name; + item.name = progressRecord.entityName; } } @@ -106,6 +107,7 @@ export class ListItemsFactory { isExpired: false, timeLeftToComplete: null, isInActivityFlow: false, + activityFlowDetails: null, }; if (isFlow) { diff --git a/src/widgets/survey/ui/Intermediate.tsx b/src/widgets/survey/ui/Intermediate.tsx index f9fc3328e..d41622806 100644 --- a/src/widgets/survey/ui/Intermediate.tsx +++ b/src/widgets/survey/ui/Intermediate.tsx @@ -133,7 +133,6 @@ function Intermediate({ eventId, pipelineActivityOrder: activitiesPassed, totalActivities, - flowName: flowName || '[Name unknown]', }), ); }, [