diff --git a/src/events/CommonEventEmitter.ts b/src/events/CommonEventEmitter.ts index f635ac3227..1ec16cc5e3 100644 --- a/src/events/CommonEventEmitter.ts +++ b/src/events/CommonEventEmitter.ts @@ -6,7 +6,7 @@ export enum CommonEvent { CommonCreated = "common-created", CommonUpdated = "common-updated", CommonDeleted = "common-deleted", - ProjectCreated = "project-created", + ProjectCreatedOrUpdated = "project-created-or-updated", ProjectUpdated = "project-updated", } @@ -14,7 +14,9 @@ export interface CommonEventToListener { [CommonEvent.CommonCreated]: (common: Common) => void; [CommonEvent.CommonUpdated]: (common: Common) => void; [CommonEvent.CommonDeleted]: (deletedCommonId: string) => void; - [CommonEvent.ProjectCreated]: (projectsStateItem: ProjectsStateItem) => void; + [CommonEvent.ProjectCreatedOrUpdated]: ( + projectsStateItem: ProjectsStateItem, + ) => void; [CommonEvent.ProjectUpdated]: ( projectsStateItem: { commonId: string } & Partial< Omit diff --git a/src/pages/App/handlers/CommonHandler/CommonHandler.tsx b/src/pages/App/handlers/CommonHandler/CommonHandler.tsx index 311b42763a..36f75d4236 100644 --- a/src/pages/App/handlers/CommonHandler/CommonHandler.tsx +++ b/src/pages/App/handlers/CommonHandler/CommonHandler.tsx @@ -41,20 +41,21 @@ const CommonHandler: FC = () => { }, []); useEffect(() => { - const handler: CommonEventToListener[CommonEvent.ProjectCreated] = ( - projectsStateItem, - ) => { - dispatch( - multipleSpacesLayoutActions.addProjectToBreadcrumbs(projectsStateItem), - ); - dispatch(commonLayoutActions.addProject(projectsStateItem)); - dispatch(projectsActions.addProject(projectsStateItem)); - }; + const handler: CommonEventToListener[CommonEvent.ProjectCreatedOrUpdated] = + (projectsStateItem) => { + dispatch( + multipleSpacesLayoutActions.addOrUpdateProjectInBreadcrumbs( + projectsStateItem, + ), + ); + dispatch(commonLayoutActions.addOrUpdateProject(projectsStateItem)); + dispatch(projectsActions.addOrUpdateProject(projectsStateItem)); + }; - CommonEventEmitter.on(CommonEvent.ProjectCreated, handler); + CommonEventEmitter.on(CommonEvent.ProjectCreatedOrUpdated, handler); return () => { - CommonEventEmitter.off(CommonEvent.ProjectCreated, handler); + CommonEventEmitter.off(CommonEvent.ProjectCreatedOrUpdated, handler); }; }, []); diff --git a/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx b/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx index 389107b0c8..7548b94acb 100644 --- a/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx +++ b/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx @@ -289,7 +289,10 @@ export default function CreateCommonModal(props: CreateCommonModalProps) { notificationsAmount: 0, }; - CommonEventEmitter.emit(CommonEvent.ProjectCreated, projectsStateItem); + CommonEventEmitter.emit( + CommonEvent.ProjectCreatedOrUpdated, + projectsStateItem, + ); CommonEventEmitter.emit( CommonEvent.CommonCreated, createdCommonData.common, diff --git a/src/pages/commonCreation/components/CommonCreation/CommonCreation.tsx b/src/pages/commonCreation/components/CommonCreation/CommonCreation.tsx index d48ded7561..b5bc88bfe7 100644 --- a/src/pages/commonCreation/components/CommonCreation/CommonCreation.tsx +++ b/src/pages/commonCreation/components/CommonCreation/CommonCreation.tsx @@ -24,7 +24,7 @@ const CommonCreation: FC = () => { (circle) => circle.allowedActions[GovernanceActions.CREATE_PROJECT], ); - CommonEventEmitter.emit(CommonEvent.ProjectCreated, { + CommonEventEmitter.emit(CommonEvent.ProjectCreatedOrUpdated, { commonId: common.id, image: common.image, name: common.name, diff --git a/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx b/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx index 6a71f896da..72c1f6112d 100644 --- a/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx +++ b/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx @@ -69,7 +69,10 @@ const ProjectCreation: FC = (props) => { }; dispatch(commonActions.setIsNewProjectCreated(true)); - CommonEventEmitter.emit(CommonEvent.ProjectCreated, projectsStateItem); + CommonEventEmitter.emit( + CommonEvent.ProjectCreatedOrUpdated, + projectsStateItem, + ); } CommonEventEmitter.emit(CommonEvent.CommonUpdated, createdProject); history.push(getCommonPagePath(createdProject.id)); diff --git a/src/services/Common.ts b/src/services/Common.ts index b9c88e2a57..925591a04b 100644 --- a/src/services/Common.ts +++ b/src/services/Common.ts @@ -17,6 +17,7 @@ import { } from "@/shared/models"; import { convertObjectDatesToFirestoreTimestamps, + emptyFunction, firestoreDataConverter, transformFirebaseDataList, } from "@/shared/utils"; @@ -296,6 +297,10 @@ class CommonService { parentCommonId: string, callback: (data: { common: Common; isRemoved: boolean }[]) => void, ): UnsubscribeFunction => { + if (!parentCommonId) { + return emptyFunction; + } + const query = firebase .firestore() .collection(Collection.Daos) diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts index 5633af13bf..3e7b0fb903 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts @@ -19,6 +19,7 @@ import { getParentItemIds, Item, } from "../../../../SidenavLayout/components/SidenavContent/components"; +import { useProjectsSubscription } from "./useProjectsSubscription"; interface Return { currentCommonId: string | null; @@ -83,6 +84,12 @@ export const useProjectsData = (): Return => { const itemIdWithNewProjectCreation = getItemIdWithNewProjectCreationByPath( location.pathname, ); + useProjectsSubscription({ + activeItemId, + areProjectsFetched, + commons, + projects, + }); useEffect(() => { if (isAuthenticated) { diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsSubscription.ts b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsSubscription.ts new file mode 100644 index 0000000000..54797df4b4 --- /dev/null +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsSubscription.ts @@ -0,0 +1,137 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { CommonEvent, CommonEventEmitter } from "@/events"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { CommonService, GovernanceService, Logger } from "@/services"; +import { GovernanceActions } from "@/shared/constants"; +import { Common } from "@/shared/models"; +import { generateCirclesDataForCommonMember } from "@/shared/utils"; +import { ProjectsStateItem } from "@/store/states"; + +interface Data { + activeItemId: string; + areProjectsFetched: boolean; + commons: ProjectsStateItem[]; + projects: ProjectsStateItem[]; +} + +const getProjectItemFromCommon = async ( + common: Common, + userId?: string, + initialItem?: ProjectsStateItem, +): Promise => { + const baseItem: Omit< + ProjectsStateItem, + "hasMembership" | "hasPermissionToAddProject" + > = { + commonId: common.id, + image: common.image, + name: common.name, + directParent: common.directParent, + }; + + if (initialItem) { + return { + ...baseItem, + hasMembership: initialItem.hasMembership, + hasPermissionToAddProject: initialItem.hasPermissionToAddProject, + }; + } + if (!userId) { + return { + ...baseItem, + hasMembership: false, + hasPermissionToAddProject: false, + }; + } + + const [governance, commonMember] = await Promise.all([ + GovernanceService.getGovernanceByCommonId(common.id), + CommonService.getCommonMemberByUserId(common.id, userId), + ]); + + return { + ...baseItem, + hasMembership: Boolean(commonMember), + hasPermissionToAddProject: Boolean( + governance && + commonMember && + generateCirclesDataForCommonMember( + governance.circles, + commonMember.circleIds, + ).allowedActions[GovernanceActions.CREATE_PROJECT], + ), + }; +}; + +export const useProjectsSubscription = (data: Data) => { + const { activeItemId, areProjectsFetched, commons, projects } = data; + const [updatedItemsQueue, setUpdatedItemsQueue] = useState([]); + const user = useSelector(selectUser()); + const userId = user?.uid; + const nextUpdatedItems = updatedItemsQueue[0]; + + useEffect(() => { + if (!areProjectsFetched) { + return; + } + + const unsubscribe = CommonService.subscribeToSubCommons( + activeItemId, + (data) => { + const commons = data.reduce((acc, { common, isRemoved }) => { + if (!isRemoved) { + CommonEventEmitter.emit(CommonEvent.CommonUpdated, common); + return acc.concat(common); + } + + return acc; + }, []); + + if (commons.length !== 0) { + setUpdatedItemsQueue((currentItems) => + currentItems.concat([commons]), + ); + } + }, + ); + + return unsubscribe; + }, [areProjectsFetched, activeItemId]); + + useEffect(() => { + if (!nextUpdatedItems) { + return; + } + if (nextUpdatedItems.length === 0) { + setUpdatedItemsQueue((currentItems) => currentItems.slice(1)); + return; + } + + (async () => { + try { + const items = await Promise.all( + nextUpdatedItems.map(async (nextUpdatedItem) => { + const existingItem = + commons.find((item) => item.commonId === nextUpdatedItem.id) || + projects.find((item) => item.commonId === nextUpdatedItem.id); + + return await getProjectItemFromCommon( + nextUpdatedItem, + userId, + existingItem, + ); + }), + ); + + items.forEach((item) => { + CommonEventEmitter.emit(CommonEvent.ProjectCreatedOrUpdated, item); + }); + } catch (error) { + Logger.error(error); + } finally { + setUpdatedItemsQueue((currentItems) => currentItems.slice(1)); + } + })(); + }, [nextUpdatedItems]); +}; diff --git a/src/store/states/commonLayout/actions.ts b/src/store/states/commonLayout/actions.ts index c42fdf08d7..93078619a1 100644 --- a/src/store/states/commonLayout/actions.ts +++ b/src/store/states/commonLayout/actions.ts @@ -21,8 +21,8 @@ export const getProjects = createAsyncAction( CommonLayoutActionType.GET_PROJECTS_FAILURE, )(); -export const addProject = createStandardAction( - CommonLayoutActionType.ADD_PROJECT, +export const addOrUpdateProject = createStandardAction( + CommonLayoutActionType.ADD_OR_UPDATE_PROJECT, )(); export const updateCommonOrProject = createStandardAction( diff --git a/src/store/states/commonLayout/constants.ts b/src/store/states/commonLayout/constants.ts index 386bb3a441..38f9c2f751 100644 --- a/src/store/states/commonLayout/constants.ts +++ b/src/store/states/commonLayout/constants.ts @@ -7,7 +7,7 @@ export enum CommonLayoutActionType { GET_PROJECTS_SUCCESS = "@COMMON_LAYOUT/GET_PROJECTS_SUCCESS", GET_PROJECTS_FAILURE = "@COMMON_LAYOUT/GET_PROJECTS_FAILURE", - ADD_PROJECT = "@COMMON_LAYOUT/ADD_PROJECT", + ADD_OR_UPDATE_PROJECT = "@COMMON_LAYOUT/ADD_OR_UPDATE_PROJECT", UPDATE_COMMON_OR_PROJECT = "@COMMON_LAYOUT/UPDATE_COMMON_OR_PROJECT", diff --git a/src/store/states/commonLayout/reducer.ts b/src/store/states/commonLayout/reducer.ts index 402c73cd18..6ee068348b 100644 --- a/src/store/states/commonLayout/reducer.ts +++ b/src/store/states/commonLayout/reducer.ts @@ -3,7 +3,7 @@ import { WritableDraft } from "immer/dist/types/types-external"; import { ActionType, createReducer } from "typesafe-actions"; import { getAllNestedItems } from "../projects/utils"; import * as actions from "./actions"; -import { CommonLayoutState } from "./types"; +import { CommonLayoutState, ProjectsStateItem } from "./types"; type Action = ActionType; @@ -26,6 +26,37 @@ const clearData = (state: WritableDraft): void => { state.areProjectsFetched = false; }; +const updateCommonOrProject = ( + state: WritableDraft, + payload: { commonId: string } & Partial>, +): boolean => { + const commonItemIndex = state.commons.findIndex( + (item) => item.commonId === payload.commonId, + ); + + if (commonItemIndex > -1) { + state.commons[commonItemIndex] = { + ...state.commons[commonItemIndex], + ...payload, + }; + return true; + } + + const projectItemIndex = state.projects.findIndex( + (item) => item.commonId === payload.commonId, + ); + + if (projectItemIndex > -1) { + state.projects[projectItemIndex] = { + ...state.projects[projectItemIndex], + ...payload, + }; + return true; + } + + return false; +}; + export const reducer = createReducer(initialState) .handleAction(actions.getCommons.request, (state) => produce(state, (nextState) => { @@ -67,8 +98,14 @@ export const reducer = createReducer(initialState) nextState.areProjectsFetched = true; }), ) - .handleAction(actions.addProject, (state, { payload }) => + .handleAction(actions.addOrUpdateProject, (state, { payload }) => produce(state, (nextState) => { + const isUpdated = updateCommonOrProject(nextState, payload); + + if (isUpdated) { + return; + } + if (!payload.directParent) { nextState.commons.push(payload); } else { @@ -78,28 +115,7 @@ export const reducer = createReducer(initialState) ) .handleAction(actions.updateCommonOrProject, (state, { payload }) => produce(state, (nextState) => { - const commonItemIndex = nextState.commons.findIndex( - (item) => item.commonId === payload.commonId, - ); - - if (commonItemIndex > -1) { - nextState.commons[commonItemIndex] = { - ...nextState.commons[commonItemIndex], - ...payload, - }; - return; - } - - const projectItemIndex = nextState.projects.findIndex( - (item) => item.commonId === payload.commonId, - ); - - if (projectItemIndex > -1) { - nextState.projects[projectItemIndex] = { - ...nextState.projects[projectItemIndex], - ...payload, - }; - } + updateCommonOrProject(nextState, payload); }), ) .handleAction(actions.setCurrentCommonId, (state, { payload }) => diff --git a/src/store/states/multipleSpacesLayout/actions.ts b/src/store/states/multipleSpacesLayout/actions.ts index 04b9f3fa97..cb8210e550 100644 --- a/src/store/states/multipleSpacesLayout/actions.ts +++ b/src/store/states/multipleSpacesLayout/actions.ts @@ -40,8 +40,8 @@ export const moveBreadcrumbsToPrevious = createStandardAction( MultipleSpacesLayoutActionType.MOVE_BREADCRUMBS_TO_PREVIOUS, )(); -export const addProjectToBreadcrumbs = createStandardAction( - MultipleSpacesLayoutActionType.ADD_PROJECT_TO_BREADCRUMBS, +export const addOrUpdateProjectInBreadcrumbs = createStandardAction( + MultipleSpacesLayoutActionType.ADD_OR_UPDATE_PROJECT_IN_BREADCRUMBS, )(); export const updateProjectInBreadcrumbs = createStandardAction( diff --git a/src/store/states/multipleSpacesLayout/constants.ts b/src/store/states/multipleSpacesLayout/constants.ts index cf1580c9aa..7c5574dedb 100644 --- a/src/store/states/multipleSpacesLayout/constants.ts +++ b/src/store/states/multipleSpacesLayout/constants.ts @@ -12,7 +12,7 @@ export enum MultipleSpacesLayoutActionType { MOVE_BREADCRUMBS_TO_PREVIOUS = "@MULTIPLE_SPACES_LAYOUT/MOVE_BREADCRUMBS_TO_PREVIOUS", - ADD_PROJECT_TO_BREADCRUMBS = "@MULTIPLE_SPACES_LAYOUT/ADD_PROJECT_TO_BREADCRUMBS", + ADD_OR_UPDATE_PROJECT_IN_BREADCRUMBS = "@MULTIPLE_SPACES_LAYOUT/ADD_OR_UPDATE_PROJECT_IN_BREADCRUMBS", UPDATE_PROJECT_IN_BREADCRUMBS = "@MULTIPLE_SPACES_LAYOUT/UPDATE_PROJECT_IN_BREADCRUMBS", diff --git a/src/store/states/multipleSpacesLayout/reducer.ts b/src/store/states/multipleSpacesLayout/reducer.ts index de00b83de0..347ae0914f 100644 --- a/src/store/states/multipleSpacesLayout/reducer.ts +++ b/src/store/states/multipleSpacesLayout/reducer.ts @@ -1,9 +1,10 @@ import produce from "immer"; +import { WritableDraft } from "immer/dist/types/types-external"; import { cloneDeep } from "lodash"; import { ActionType, createReducer } from "typesafe-actions"; import { InboxItemType } from "@/shared/constants"; import * as actions from "./actions"; -import { MultipleSpacesLayoutState } from "./types"; +import { MultipleSpacesLayoutState, ProjectsStateItem } from "./types"; type Action = ActionType; @@ -14,6 +15,26 @@ const initialState: MultipleSpacesLayoutState = { mainWidth: window.innerWidth, }; +const updateProjectInBreadcrumbs = ( + state: WritableDraft, + payload: { commonId: string } & Partial>, +): void => { + if (state.breadcrumbs?.type !== InboxItemType.FeedItemFollow) { + return; + } + + const itemIndex = state.breadcrumbs.items.findIndex( + (item) => item.commonId === payload.commonId, + ); + + if (itemIndex > -1) { + state.breadcrumbs.items[itemIndex] = { + ...state.breadcrumbs.items[itemIndex], + ...payload, + }; + } +}; + export const reducer = createReducer( initialState, ) @@ -33,32 +54,29 @@ export const reducer = createReducer( nextState.breadcrumbs = null; }), ) - .handleAction(actions.addProjectToBreadcrumbs, (state, { payload }) => - produce(state, (nextState) => { - if (nextState.breadcrumbs?.type === InboxItemType.FeedItemFollow) { - nextState.breadcrumbs.items = - nextState.breadcrumbs.items.concat(payload); - } - }), - ) - .handleAction(actions.updateProjectInBreadcrumbs, (state, { payload }) => + .handleAction(actions.addOrUpdateProjectInBreadcrumbs, (state, { payload }) => produce(state, (nextState) => { if (nextState.breadcrumbs?.type !== InboxItemType.FeedItemFollow) { return; } - const itemIndex = nextState.breadcrumbs.items.findIndex( + const isItemFound = nextState.breadcrumbs.items.some( (item) => item.commonId === payload.commonId, ); - if (itemIndex > -1) { - nextState.breadcrumbs.items[itemIndex] = { - ...nextState.breadcrumbs.items[itemIndex], - ...payload, - }; + if (isItemFound) { + updateProjectInBreadcrumbs(nextState, payload); + } else { + nextState.breadcrumbs.items = + nextState.breadcrumbs.items.concat(payload); } }), ) + .handleAction(actions.updateProjectInBreadcrumbs, (state, { payload }) => + produce(state, (nextState) => { + updateProjectInBreadcrumbs(nextState, payload); + }), + ) .handleAction(actions.setBackUrl, (state, { payload }) => produce(state, (nextState) => { nextState.backUrl = payload; diff --git a/src/store/states/projects/actions.ts b/src/store/states/projects/actions.ts index 989170ad66..0e879efe86 100644 --- a/src/store/states/projects/actions.ts +++ b/src/store/states/projects/actions.ts @@ -8,8 +8,8 @@ export const getProjects = createAsyncAction( ProjectsActionType.GET_PROJECTS_FAILURE, )(); -export const addProject = createStandardAction( - ProjectsActionType.ADD_PROJECT, +export const addOrUpdateProject = createStandardAction( + ProjectsActionType.ADD_OR_UPDATE_PROJECT, )(); export const updateProject = createStandardAction( diff --git a/src/store/states/projects/constants.ts b/src/store/states/projects/constants.ts index e25bfb1d5f..0a8ae4942f 100644 --- a/src/store/states/projects/constants.ts +++ b/src/store/states/projects/constants.ts @@ -3,7 +3,7 @@ export enum ProjectsActionType { GET_PROJECTS_SUCCESS = "@PROJECTS/GET_PROJECTS_SUCCESS", GET_PROJECTS_FAILURE = "@PROJECTS/GET_PROJECTS_FAILURE", - ADD_PROJECT = "@PROJECTS/ADD_PROJECT", + ADD_OR_UPDATE_PROJECT = "@PROJECTS/ADD_OR_UPDATE_PROJECT", UPDATE_PROJECT = "@PROJECTS/UPDATE_PROJECT", diff --git a/src/store/states/projects/reducer.ts b/src/store/states/projects/reducer.ts index ed803c59fa..2a5195e86d 100644 --- a/src/store/states/projects/reducer.ts +++ b/src/store/states/projects/reducer.ts @@ -2,7 +2,7 @@ import produce from "immer"; import { WritableDraft } from "immer/dist/types/types-external"; import { ActionType, createReducer } from "typesafe-actions"; import * as actions from "./actions"; -import { ProjectsState } from "./types"; +import { ProjectsState, ProjectsStateItem } from "./types"; import { getAllNestedItems, getRelatedToIdItems } from "./utils"; type Action = ActionType; @@ -20,6 +20,25 @@ const clearProjects = (state: WritableDraft): void => { state.isDataFetched = false; }; +const updateProject = ( + state: WritableDraft, + payload: { commonId: string } & Partial>, +): boolean => { + const itemIndex = state.data.findIndex( + (item) => item.commonId === payload.commonId, + ); + + if (itemIndex > -1) { + state.data[itemIndex] = { + ...state.data[itemIndex], + ...payload, + }; + return true; + } + + return false; +}; + export const reducer = createReducer(initialState) .handleAction(actions.getProjects.request, (state) => produce(state, (nextState) => { @@ -40,23 +59,18 @@ export const reducer = createReducer(initialState) nextState.isDataFetched = true; }), ) - .handleAction(actions.addProject, (state, { payload }) => + .handleAction(actions.addOrUpdateProject, (state, { payload }) => produce(state, (nextState) => { - nextState.data.push(payload); + const isUpdated = updateProject(nextState, payload); + + if (!isUpdated) { + nextState.data.push(payload); + } }), ) .handleAction(actions.updateProject, (state, { payload }) => produce(state, (nextState) => { - const itemIndex = nextState.data.findIndex( - (item) => item.commonId === payload.commonId, - ); - - if (itemIndex > -1) { - nextState.data[itemIndex] = { - ...nextState.data[itemIndex], - ...payload, - }; - } + updateProject(nextState, payload); }), ) .handleAction(actions.clearProjects, (state) =>