From f1e3e9f10351e53843ea5162c050c34dfc5a6179 Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Thu, 1 Feb 2024 21:25:07 +0200 Subject: [PATCH 01/10] feat: useTimer hook --- src/shared/utils/index.ts | 1 + src/shared/utils/useTimer.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/shared/utils/useTimer.ts diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 15f5f6968..57a33d881 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -13,3 +13,4 @@ export * from "./helpers" export * from "./eventEmitter" export * from "./dictionary.map" export * from "./mixpanel" +export * from "./useTimer" diff --git a/src/shared/utils/useTimer.ts b/src/shared/utils/useTimer.ts new file mode 100644 index 000000000..d5a8f9fc8 --- /dev/null +++ b/src/shared/utils/useTimer.ts @@ -0,0 +1,29 @@ +import { useCallback, useRef } from "react" + +type SetTimerProps = { + callback: () => void + delay: number +} + +export const useTimer = () => { + const timerRef = useRef(undefined) + + // this resets the timer if it exists. + const resetTimer = useCallback(() => { + if (timerRef) window.clearTimeout(timerRef.current) + }, [timerRef]) + + const setTimer = useCallback( + (props: SetTimerProps) => { + timerRef.current = window.setTimeout(() => { + // clears any pending timer. + resetTimer() + + props.callback() + }, props.delay) + }, + [resetTimer], + ) + + return { setTimer, resetTimer } +} From f78aa0eb0e9302da611118d4f77600182fa13b6a Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Thu, 1 Feb 2024 21:28:55 +0200 Subject: [PATCH 02/10] refactor: InactivityTracker --- .../ProtectedRoute/InactivityTracker.tsx | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/widgets/ProtectedRoute/InactivityTracker.tsx b/src/widgets/ProtectedRoute/InactivityTracker.tsx index d55aa9fe9..258b1daf9 100644 --- a/src/widgets/ProtectedRoute/InactivityTracker.tsx +++ b/src/widgets/ProtectedRoute/InactivityTracker.tsx @@ -1,6 +1,7 @@ -import { PropsWithChildren, useCallback, useEffect, useRef } from "react" +import { PropsWithChildren, useCallback, useEffect } from "react" import { useLogout } from "~/features/Logout" +import { useTimer } from "~/shared/utils" export type InactivityTrackerProps = PropsWithChildren @@ -11,33 +12,24 @@ const ONE_MIN = 60 * ONE_SEC const LOGOUT_TIME_LIMIT = 15 * ONE_MIN // 15 min export const InactivityTracker = ({ children }: InactivityTrackerProps) => { - const timerRef = useRef(undefined) const { logout } = useLogout() - // this resets the timer if it exists. - const resetTimer = useCallback(() => { - if (timerRef) window.clearTimeout(timerRef.current) - }, [timerRef]) + const { resetTimer, setTimer } = useTimer() - const logoutTimer = useCallback(() => { - timerRef.current = window.setTimeout(() => { - // clears any pending timer. - resetTimer() - - // Listener clean up. Removes the existing event listener from the window - Object.values(events).forEach(item => { - window.removeEventListener(item, resetTimer) - }) + const onLogoutTimerExpire = useCallback(() => { + // Listener clean up. Removes the existing event listener from the window + Object.values(events).forEach(item => { + window.removeEventListener(item, resetTimer) + }) - // logs out user - logout() - }, LOGOUT_TIME_LIMIT) - }, [resetTimer, logout]) + // logs out user + logout() + }, [logout, resetTimer]) const onActivityEventHandler = useCallback(() => { resetTimer() - logoutTimer() - }, [resetTimer, logoutTimer]) + setTimer({ delay: LOGOUT_TIME_LIMIT, callback: onLogoutTimerExpire }) + }, [resetTimer, setTimer, onLogoutTimerExpire]) useEffect(() => { Object.values(events).forEach(item => { From 6e8f2a9bf19d645a2a4f682d75eb245c9f60a124 Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Fri, 2 Feb 2024 00:29:27 +0200 Subject: [PATCH 03/10] fix: getAllUserEvents return type --- src/shared/api/services/events.service.ts | 9 +++++++-- src/shared/api/types/applet.ts | 5 +++++ src/shared/api/types/events.ts | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/shared/api/services/events.service.ts b/src/shared/api/services/events.service.ts index b0ff788df..891f1f256 100644 --- a/src/shared/api/services/events.service.ts +++ b/src/shared/api/services/events.service.ts @@ -1,5 +1,10 @@ import axiosService from "./axios" -import { GetEventsByAppletIdPayload, GetEventsByPublicAppletKey, SuccessEventsByAppletIdResponse } from "../types" +import { + GetEventsByAppletIdPayload, + GetEventsByPublicAppletKey, + SuccessAllUserEventsResponse, + SuccessEventsByAppletIdResponse, +} from "../types" function eventService() { return { @@ -7,7 +12,7 @@ function eventService() { return axiosService.get(`/users/me/events/${payload.appletId}`) }, getUserEvents() { - return axiosService.get("/users/me/events") + return axiosService.get("/users/me/events") }, getEventsByPublicAppletKey(payload: GetEventsByPublicAppletKey) { return axiosService.get(`/public/applets/${payload.publicAppletKey}/events`) diff --git a/src/shared/api/types/applet.ts b/src/shared/api/types/applet.ts index 1f500078c..46b91aab4 100644 --- a/src/shared/api/types/applet.ts +++ b/src/shared/api/types/applet.ts @@ -125,6 +125,11 @@ export type AppletEventsResponse = { events: ScheduleEventDto[] } +export type AllUserEventsDTO = { + appletId: string + events: ScheduleEventDto[] +} + export type AppletEncryptionDTO = { accountId: string base: string // Contains number[] diff --git a/src/shared/api/types/events.ts b/src/shared/api/types/events.ts index a473bb5af..f69f788b8 100644 --- a/src/shared/api/types/events.ts +++ b/src/shared/api/types/events.ts @@ -1,4 +1,4 @@ -import { AppletEventsResponse } from "./applet" +import { AllUserEventsDTO, AppletEventsResponse } from "./applet" import { BaseSuccessResponse } from "./base" import { HourMinute } from "../../utils" @@ -14,6 +14,7 @@ export type GetEventsByPublicAppletKey = { } export type SuccessEventsByAppletIdResponse = BaseSuccessResponse +export type SuccessAllUserEventsResponse = BaseSuccessResponse export type EventsByAppletIdResponseDTO = { appletId: string From 14626dc19e0880c9007945118650df6cdc2e8430 Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Fri, 2 Feb 2024 00:30:53 +0200 Subject: [PATCH 04/10] feat: matchPaths function --- src/shared/utils/index.ts | 1 + src/shared/utils/react-routes.ts | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/shared/utils/react-routes.ts diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 57a33d881..66a51068b 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -14,3 +14,4 @@ export * from "./eventEmitter" export * from "./dictionary.map" export * from "./mixpanel" export * from "./useTimer" +export * from "./react-routes" diff --git a/src/shared/utils/react-routes.ts b/src/shared/utils/react-routes.ts new file mode 100644 index 000000000..ebbede444 --- /dev/null +++ b/src/shared/utils/react-routes.ts @@ -0,0 +1,22 @@ +import { matchPath } from "react-router-dom" + +type Params = { + end?: boolean + caseSensitive?: boolean +} + +export const matchPaths = ( + patterns: string[], + pathname: string, + params: Params = { end: false, caseSensitive: false }, +) => { + return patterns.map(path => + matchPath( + { + path, + ...params, + }, + pathname, + ), + ) +} From 414c5c7ba56163faff9b27863f20587bc112cd35 Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Fri, 2 Feb 2024 11:26:32 +0200 Subject: [PATCH 05/10] feat: getDataFromProgressId method --- src/abstract/lib/getProgressId.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/abstract/lib/getProgressId.ts b/src/abstract/lib/getProgressId.ts index 4aa85495a..3f97067a5 100644 --- a/src/abstract/lib/getProgressId.ts +++ b/src/abstract/lib/getProgressId.ts @@ -1,3 +1,9 @@ export const getProgressId = (entityId: string, eventId: string): string => { return `${entityId}/${eventId}` } + +export const getDataFromProgressId = (progressId: string): { entityId: string; eventId: string } => { + const [entityId, eventId] = progressId.split("/") + + return { entityId, eventId } +} From 018ee815c26a44e9fac012df16d718fb4b03d81d Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Fri, 2 Feb 2024 11:29:20 +0200 Subject: [PATCH 06/10] feat: useUserEventsMutation --- src/entities/event/api/index.ts | 1 + src/entities/event/api/useUserEventsMutation.ts | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 src/entities/event/api/useUserEventsMutation.ts diff --git a/src/entities/event/api/index.ts b/src/entities/event/api/index.ts index 347cf2b5a..d266e622e 100644 --- a/src/entities/event/api/index.ts +++ b/src/entities/event/api/index.ts @@ -1 +1,2 @@ export * from "./useEventsByAppletIdQuery" +export * from "./useUserEventsMutation" diff --git a/src/entities/event/api/useUserEventsMutation.ts b/src/entities/event/api/useUserEventsMutation.ts new file mode 100644 index 000000000..94fdd9591 --- /dev/null +++ b/src/entities/event/api/useUserEventsMutation.ts @@ -0,0 +1,7 @@ +import { MutationOptions, eventsService, useBaseMutation } from "~/shared/api" + +type Options = MutationOptions + +export const useUserEventsMutation = (options?: Options) => { + return useBaseMutation(["getUserEvents"], eventsService.getUserEvents, { ...options }) +} From f62eda5a9d5c9a73221aa04109a1b56eb3915a2e Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Fri, 2 Feb 2024 11:31:26 +0200 Subject: [PATCH 07/10] feat: useEntityProgressAutoCompletion --- .../EntityProgressAutoCompletion/index.ts | 1 + .../lib/useEntityProgressAutoCompletion.ts | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/features/EntityProgressAutoCompletion/index.ts create mode 100644 src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts diff --git a/src/features/EntityProgressAutoCompletion/index.ts b/src/features/EntityProgressAutoCompletion/index.ts new file mode 100644 index 000000000..158607070 --- /dev/null +++ b/src/features/EntityProgressAutoCompletion/index.ts @@ -0,0 +1 @@ +export * from "./lib/useEntityProgressAutoCompletion" diff --git a/src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts b/src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts new file mode 100644 index 000000000..3ffa995a8 --- /dev/null +++ b/src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts @@ -0,0 +1,40 @@ +import { useCallback, useEffect } from "react" + +import { useLocation } from "react-router-dom" + +import { appletModel } from "~/entities/applet" +import { useUserEventsMutation } from "~/entities/event" +import { ROUTES } from "~/shared/constants" +import { matchPaths } from "~/shared/utils" +import { useAppSelector } from "~/shared/utils" + +const AVAILABLE_PATHS = [ROUTES.profile.path, ROUTES.settings.path, ROUTES.appletList.path] + +export const useEntityProgressAutoCompletion = () => { + const location = useLocation() + + const { mutateAsync } = useUserEventsMutation() + + const applets = useAppSelector(appletModel.selectors.appletsSelector) + + const isProgressEmpty = Object.keys(applets.groupProgress).length === 0 + + const isValidPath = matchPaths(AVAILABLE_PATHS, location.pathname).some(Boolean) + + const performAutoCompletion = useCallback(async () => { + // Here logic for auto completion + + // Step 1: Get all events from request + const userEventsData = await mutateAsync(undefined) + const userEvents = userEventsData.data?.result + + console.log(userEvents) + // Step 2: Get all groupProgress by AvailabilityGroupBuilder + }, [mutateAsync]) + + useEffect(() => { + if (isValidPath && !isProgressEmpty) { + performAutoCompletion() + } + }, [isProgressEmpty, isValidPath, performAutoCompletion]) +} From cdeee5dddc1217acfac7e40037132ef566077489 Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Fri, 2 Feb 2024 11:33:11 +0200 Subject: [PATCH 08/10] feat: use useEntityProgressAutoCompletion in ApplicationRouter --- src/pages/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 48bfb5b0d..108bd4f47 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -4,10 +4,13 @@ const UnauthorizedRoutes = lazy(() => import("./UnauthorizedRoutes")) const AuthorizedRoutes = lazy(() => import("./AuthorizedRoutes")) import { userModel } from "~/entities/user" +import { useEntityProgressAutoCompletion } from "~/features/EntityProgressAutoCompletion" function ApplicationRouter(): JSX.Element | null { const { isAuthorized, tokens } = userModel.hooks.useAuthorization() + useEntityProgressAutoCompletion() + if (isAuthorized) { return } From 4f48f961ba4840879c68ff041075357c804d3599 Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Fri, 2 Feb 2024 18:03:49 +0200 Subject: [PATCH 09/10] Remove react-routes --- src/shared/utils/react-routes.ts | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/shared/utils/react-routes.ts diff --git a/src/shared/utils/react-routes.ts b/src/shared/utils/react-routes.ts deleted file mode 100644 index ebbede444..000000000 --- a/src/shared/utils/react-routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { matchPath } from "react-router-dom" - -type Params = { - end?: boolean - caseSensitive?: boolean -} - -export const matchPaths = ( - patterns: string[], - pathname: string, - params: Params = { end: false, caseSensitive: false }, -) => { - return patterns.map(path => - matchPath( - { - path, - ...params, - }, - pathname, - ), - ) -} From a0a2de5e5d7abe5392258363504aace32a0c69bf Mon Sep 17 00:00:00 2001 From: Viktor Riabkov Date: Sat, 3 Feb 2024 08:33:37 +0200 Subject: [PATCH 10/10] Provide raw example of implementation --- .../lib/GroupBuilder/buildIdToEntityMap.ts | 8 +++ src/abstract/lib/GroupBuilder/index.ts | 1 + .../lib/useEntityProgressAutoCompletion.ts | 71 ++++++++++++++++--- .../services/ActivityGroupsBuildManager.ts | 9 +-- 4 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 src/abstract/lib/GroupBuilder/buildIdToEntityMap.ts diff --git a/src/abstract/lib/GroupBuilder/buildIdToEntityMap.ts b/src/abstract/lib/GroupBuilder/buildIdToEntityMap.ts new file mode 100644 index 000000000..52a85deb6 --- /dev/null +++ b/src/abstract/lib/GroupBuilder/buildIdToEntityMap.ts @@ -0,0 +1,8 @@ +import { Activity, ActivityFlow, Entity } from "." + +export const buildIdToEntityMap = (activities: Activity[], activityFlows?: ActivityFlow[]): Record => { + return [...activities, ...(activityFlows ?? [])].reduce>((acc, current) => { + acc[current.id] = current + return acc + }, {}) +} diff --git a/src/abstract/lib/GroupBuilder/index.ts b/src/abstract/lib/GroupBuilder/index.ts index 9676f6721..8141f540c 100644 --- a/src/abstract/lib/GroupBuilder/index.ts +++ b/src/abstract/lib/GroupBuilder/index.ts @@ -1,3 +1,4 @@ export { createActivityGroupsBuilder } from "./ActivityGroupsBuilder" export * from "./types" export * from "./activityGroups.types" +export * from "./buildIdToEntityMap" diff --git a/src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts b/src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts index 3ffa995a8..982e45d4c 100644 --- a/src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts +++ b/src/features/EntityProgressAutoCompletion/lib/useEntityProgressAutoCompletion.ts @@ -1,36 +1,89 @@ -import { useCallback, useEffect } from "react" +import { useCallback, useEffect, useMemo } from "react" import { useLocation } from "react-router-dom" +import { getDataFromProgressId } from "~/abstract/lib" +import { EventEntity, buildIdToEntityMap } from "~/abstract/lib/GroupBuilder" +import { AvailableGroupEvaluator } from "~/abstract/lib/GroupBuilder/AvailableGroupEvaluator" +import { GroupsBuildContext } from "~/abstract/lib/GroupBuilder/GroupUtility" import { appletModel } from "~/entities/applet" import { useUserEventsMutation } from "~/entities/event" import { ROUTES } from "~/shared/constants" import { matchPaths } from "~/shared/utils" import { useAppSelector } from "~/shared/utils" +import { mapActivitiesFromDto } from "~/widgets/ActivityGroups/model/mappers" const AVAILABLE_PATHS = [ROUTES.profile.path, ROUTES.settings.path, ROUTES.appletList.path] export const useEntityProgressAutoCompletion = () => { const location = useLocation() - const { mutateAsync } = useUserEventsMutation() + const { mutateAsync: getUserEvents } = useUserEventsMutation() const applets = useAppSelector(appletModel.selectors.appletsSelector) - const isProgressEmpty = Object.keys(applets.groupProgress).length === 0 + const progressKeys = useMemo(() => Object.keys(applets.groupProgress), [applets.groupProgress]) + + const isProgressEmpty = progressKeys.length === 0 const isValidPath = matchPaths(AVAILABLE_PATHS, location.pathname).some(Boolean) - const performAutoCompletion = useCallback(async () => { + const performAutoCompletion = useCallback(() => { // Here logic for auto completion // Step 1: Get all events from request - const userEventsData = await mutateAsync(undefined) - const userEvents = userEventsData.data?.result + // const userEventsData = await getUserEvents(undefined) // I don't know about this request, I think it is not a good idea [Just example] + // const userEvents = userEventsData.data?.result + + // Step 2: Get all progress entities ids from redux + const progressEntityEvents = progressKeys.map((progressId: string) => { + return getDataFromProgressId(progressId) + }) + + console.info(progressEntityEvents) + + // Step 3: Get all activities by progress entities ids from requrest + + // Step 4: Map all activitiesDTOS into activities + const activities = mapActivitiesFromDto([]) // Provide ActivityDTO here + + const idToEntity = buildIdToEntityMap(activities) + console.info(idToEntity) + + // Step 5: Prepare input params for AvailableGroupEvaluator + const inputParams: GroupsBuildContext = { + progress: applets.groupProgress, + allAppletActivities: activities, + } + + // Step 6: Create AvailableGroupEvaluator instance + const availableEvaluator = new AvailableGroupEvaluator(inputParams) + + // Step 7: Prepate eventsEntities from userEvents and progressEntityEvents + // const events = userEvents?.reduce>((acc, ue) => acc.concat(ue.events), []) + // console.info(events) + + // This example of how to map events to EventEntity taken from src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts + const eventsEntities: Array = [] + // const eventsEntities: Array = events + // .map(event => ({ + // entity: idToEntity[event.entityId], + // event, + // })) + // // @todo - remove after fix on BE + // .filter(entityEvent => !!entityEvent.entity) + + // Step 8: Evaluate available events + const filtered = availableEvaluator.evaluate(eventsEntities) + console.info(filtered) + + // Step 9: Take events which are already unavailable + + // Step 10: Get actual activity progress from the redux "progress" - console.log(userEvents) - // Step 2: Get all groupProgress by AvailabilityGroupBuilder - }, [mutateAsync]) + // Step 11: Process answers + // Step 12: Submit answers + }, [applets.groupProgress, getUserEvents, progressKeys]) useEffect(() => { if (isValidPath && !isProgressEmpty) { diff --git a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts index c2efaf5be..779217997 100644 --- a/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts +++ b/src/widgets/ActivityGroups/model/services/ActivityGroupsBuildManager.ts @@ -5,8 +5,8 @@ import { Activity, ActivityFlow, ActivityListGroup, - Entity, EventEntity, + buildIdToEntityMap, createActivityGroupsBuilder, } from "~/abstract/lib/GroupBuilder" import { EventModel, ScheduleEvent } from "~/entities/event" @@ -24,13 +24,6 @@ type ProcessParams = { } const createActivityGroupsBuildManager = () => { - const buildIdToEntityMap = (activities: Activity[], activityFlows: ActivityFlow[]): Record => { - return [...activities, ...activityFlows].reduce>((acc, current) => { - acc[current.id] = current - return acc - }, {}) - } - const sort = (eventEntities: EventEntity[]) => { let flows = eventEntities.filter(x => x.entity.pipelineType === ActivityPipelineType.Flow) let activities = eventEntities.filter(x => x.entity.pipelineType === ActivityPipelineType.Regular)