diff --git a/src/Interfaces/IContactMessages.ts b/src/Interfaces/IContactMessages.ts
new file mode 100644
index 000000000..0a1d1c04b
--- /dev/null
+++ b/src/Interfaces/IContactMessages.ts
@@ -0,0 +1,15 @@
+import { GoalItem } from "@src/models/GoalItem";
+
+export interface SharedGoalMessage {
+ relId: string;
+ goalWithChildrens: GoalItem[];
+ lastProcessedTimestamp: string;
+ type: "shareMessage";
+ installId: string;
+ TTL: number;
+}
+
+export interface SharedGoalMessageResponse {
+ success: boolean;
+ response: SharedGoalMessage[];
+}
diff --git a/src/Interfaces/IPopupModals.ts b/src/Interfaces/IPopupModals.ts
index d5a368b8d..b8382b095 100644
--- a/src/Interfaces/IPopupModals.ts
+++ b/src/Interfaces/IPopupModals.ts
@@ -9,6 +9,7 @@ export interface TConfirmActionState {
addHint: boolean;
deleteHint: boolean;
reportHint: boolean;
+ move: boolean;
};
collaboration: {
colabRequest: boolean;
@@ -20,7 +21,7 @@ export interface TConfirmActionState {
export interface TConfirmGoalAction {
actionCategory: "goal";
- actionName: "archive" | "delete" | "shareAnonymously" | "shareWithOne" | "restore";
+ actionName: "archive" | "delete" | "shareAnonymously" | "shareWithOne" | "restore" | "move";
}
export interface TConfirmColabGoalAction {
diff --git a/src/api/GoalsAPI/index.ts b/src/api/GoalsAPI/index.ts
index b0cd37cae..73241e145 100644
--- a/src/api/GoalsAPI/index.ts
+++ b/src/api/GoalsAPI/index.ts
@@ -157,22 +157,22 @@ export const unarchiveUserGoal = async (goal: GoalItem) => {
await unarchiveGoal(goal);
};
-export const removeGoal = async (goal: GoalItem) => {
+export const removeGoal = async (goal: GoalItem, permanently = false) => {
await deleteHintItem(goal.id);
await Promise.allSettled([
db.goalsCollection.delete(goal.id).catch((err) => console.log("failed to delete", err)),
- addDeletedGoal(goal),
+ permanently ? null : addDeletedGoal(goal),
]);
};
-export const removeChildrenGoals = async (parentGoalId: string) => {
+export const removeChildrenGoals = async (parentGoalId: string, permanently = false) => {
const childrenGoals = await getChildrenGoals(parentGoalId);
if (childrenGoals.length === 0) {
return;
}
childrenGoals.forEach((goal) => {
- removeChildrenGoals(goal.id);
- removeGoal(goal);
+ removeChildrenGoals(goal.id, permanently);
+ removeGoal(goal, permanently);
});
};
@@ -312,9 +312,9 @@ export const notifyNewColabRequest = async (id: string, relId: string) => {
// });
// };
-export const removeGoalWithChildrens = async (goal: GoalItem) => {
- await removeChildrenGoals(goal.id);
- await removeGoal(goal);
+export const removeGoalWithChildrens = async (goal: GoalItem, permanently = false) => {
+ await removeChildrenGoals(goal.id, permanently);
+ await removeGoal(goal, permanently);
if (goal.parentGoalId !== "root") {
getGoal(goal.parentGoalId).then(async (parentGoal: GoalItem) => {
const parentGoalSublist = parentGoal.sublist;
diff --git a/src/api/SharedWMAPI/index.ts b/src/api/SharedWMAPI/index.ts
index 00818799c..62e59a69a 100644
--- a/src/api/SharedWMAPI/index.ts
+++ b/src/api/SharedWMAPI/index.ts
@@ -2,7 +2,8 @@
import { db } from "@models";
import { GoalItem } from "@src/models/GoalItem";
import { createGoalObjectFromTags } from "@src/helpers/GoalProcessor";
-import { addDeletedGoal, addGoal } from "../GoalsAPI";
+import { addGoal } from "../GoalsAPI";
+import { getContactByRelId } from "../ContactsAPI";
export const addSharedWMSublist = async (parentGoalId: string, goalIds: string[]) => {
db.transaction("rw", db.sharedWMCollection, async () => {
@@ -17,29 +18,53 @@ export const addSharedWMSublist = async (parentGoalId: string, goalIds: string[]
});
};
-export const addSharedWMGoal = async (goalDetails: object) => {
+export const addSharedWMGoal = async (goalDetails: GoalItem, relId = "") => {
+ console.log("[addSharedWMGoal] Input goal details:", goalDetails);
+ console.log("[addSharedWMGoal] Input relId:", relId);
+
const { participants } = goalDetails;
- const newGoal = createGoalObjectFromTags({ ...goalDetails, typeOfGoal: "shared" });
- if (participants) newGoal.participants = participants;
+ let updatedParticipants = participants || [];
+
+ if (relId) {
+ const contact = await getContactByRelId(relId);
+ if (contact) {
+ const contactExists = updatedParticipants.some((p) => p.relId === relId);
+ if (!contactExists) {
+ updatedParticipants = [...updatedParticipants, { ...contact, following: true, type: "sharer" }];
+ }
+ }
+ }
+
+ console.log("[addSharedWMGoal] Updated participants:", updatedParticipants);
+ const newGoal = createGoalObjectFromTags({
+ ...goalDetails,
+ typeOfGoal: "shared",
+ participants: updatedParticipants,
+ });
+
await db
.transaction("rw", db.sharedWMCollection, async () => {
await db.sharedWMCollection.add(newGoal);
+ console.log("[addSharedWMGoal] Goal added to sharedWMCollection");
})
.then(async () => {
const { parentGoalId } = newGoal;
if (parentGoalId !== "root") {
+ console.log("[addSharedWMGoal] Adding goal to parent sublist. ParentId:", parentGoalId);
await addSharedWMSublist(parentGoalId, [newGoal.id]);
}
})
.catch((e) => {
- console.log(e.stack || e);
+ console.error("[addSharedWMGoal] Error:", e.stack || e);
});
+
+ console.log("[addSharedWMGoal] Successfully created goal with ID:", newGoal.id);
return newGoal.id;
};
-export const addGoalsInSharedWM = async (goals: GoalItem[]) => {
+export const addGoalsInSharedWM = async (goals: GoalItem[], relId: string) => {
goals.forEach((ele) => {
- addSharedWMGoal(ele).then((res) => console.log(res, "added"));
+ addSharedWMGoal(ele, relId).then((res) => console.log(res, "added"));
});
};
@@ -83,7 +108,7 @@ export const getRootGoalsOfPartner = async (relId: string) => {
).reverse();
};
-export const updateSharedWMGoal = async (id: string, changes: object) => {
+export const updateSharedWMGoal = async (id: string, changes: Partial) => {
db.transaction("rw", db.sharedWMCollection, async () => {
await db.sharedWMCollection.update(id, changes).then((updated) => updated);
}).catch((e) => {
@@ -157,3 +182,19 @@ export const convertSharedWMGoalToColab = async (goal: GoalItem) => {
})
.catch((err) => console.log(err));
};
+
+export const updateSharedWMParentSublist = async (oldParentId: string, newParentId: string, goalId: string) => {
+ // Remove from old parent
+ const oldParentGoal = await getSharedWMGoal(oldParentId);
+ if (oldParentGoal?.sublist) {
+ const updatedOldSublist = oldParentGoal.sublist.filter((id) => id !== goalId);
+ await updateSharedWMGoal(oldParentId, { sublist: updatedOldSublist });
+ }
+
+ // Add to new parent
+ const newParentGoal = await getSharedWMGoal(newParentId);
+ if (newParentGoal) {
+ const updatedNewSublist = [...(newParentGoal.sublist || []), goalId];
+ await updateSharedWMGoal(newParentId, { sublist: updatedNewSublist });
+ }
+};
diff --git a/src/common/ConfirmationModal.tsx b/src/common/ConfirmationModal.tsx
index 4520ede48..a7120438d 100644
--- a/src/common/ConfirmationModal.tsx
+++ b/src/common/ConfirmationModal.tsx
@@ -1,7 +1,7 @@
import { Checkbox } from "antd";
import React, { useState } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { darkModeState, displayConfirmation } from "@src/store";
import { ConfirmationModalProps } from "@src/Interfaces/IPopupModals";
@@ -51,7 +51,7 @@ const ConfirmationModal: React.FC = ({ action, handleCli
{t(headerKey)}
- {t("note")}: {t(noteKey)}
+ {t("note")}: , ul:
, li: }} />
= ({ title, active }) => {
);
+ case "Move":
+ return (
+
+ );
+
default:
return ;
}
diff --git a/src/common/ZButton.tsx b/src/common/ZButton.tsx
new file mode 100644
index 000000000..2a9663155
--- /dev/null
+++ b/src/common/ZButton.tsx
@@ -0,0 +1,24 @@
+import { darkModeState } from "@src/store";
+import React from "react";
+import { useRecoilValue } from "recoil";
+
+interface ZButtonProps {
+ children: React.ReactNode;
+ onClick?: () => void;
+ className?: string;
+}
+
+const ZButton: React.FC = ({ children, onClick, className }) => {
+ const darkModeStatus = useRecoilValue(darkModeState);
+
+ const defaultClassName = `default-btn${darkModeStatus ? "-dark" : ""}`;
+ const combinedClassName = className ? `${defaultClassName} ${className}` : defaultClassName;
+
+ return (
+
+ );
+};
+
+export default ZButton;
diff --git a/src/components/BottomNavbar/BottomNavbar.tsx b/src/components/BottomNavbar/BottomNavbar.tsx
index ba408ba34..12eb605a8 100644
--- a/src/components/BottomNavbar/BottomNavbar.tsx
+++ b/src/components/BottomNavbar/BottomNavbar.tsx
@@ -64,6 +64,7 @@ const BottomNavbar = ({ title }: { title: string }) => {
}
}
};
+
const { activeGoalId } = location.state || {};
const isAddBtnVisible = title !== "Focus" && (isPartnerModeActive ? !!activeGoalId : true);
return (
diff --git a/src/components/GlobalAddBtn.tsx b/src/components/GlobalAddBtn.tsx
index b6761f0d0..da759266d 100644
--- a/src/components/GlobalAddBtn.tsx
+++ b/src/components/GlobalAddBtn.tsx
@@ -1,5 +1,5 @@
-import React, { ReactNode, useEffect } from "react";
-import { useRecoilValue } from "recoil";
+import React, { ReactNode, useEffect, useState } from "react";
+import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
@@ -17,6 +17,10 @@ import { TGoalCategory } from "@src/models/GoalItem";
import { allowAddingBudgetGoal } from "@src/store/GoalsState";
import useLongPress from "@src/hooks/useLongPress";
import { useKeyPress } from "@src/hooks/useKeyPress";
+import { moveGoalState } from "@src/store/moveGoalState";
+import { moveGoalHierarchy } from "@src/helpers/GoalController";
+import { displayToast, lastAction } from "@src/store";
+import { useParentGoalContext } from "@src/contexts/parentGoal-context";
interface AddGoalOptionProps {
children: ReactNode;
@@ -52,10 +56,20 @@ const GlobalAddBtn = ({ add }: { add: string }) => {
const { handleAddFeeling } = useFeelingStore();
const isPartnerModeActive = !!partnerId;
+ const setToastMessage = useSetRecoilState(displayToast);
+
+ const setLastAction = useSetRecoilState(lastAction);
+
+ const {
+ parentData: { parentGoal = { id: "root" } },
+ } = useParentGoalContext();
+
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const themeSelection = useRecoilValue(themeSelectionMode);
const isAddingBudgetGoalAllowed = useRecoilValue(allowAddingBudgetGoal);
+ const [goalToMove, setGoalToMove] = useRecoilState(moveGoalState);
+ const [isDisabled, setIsDisabled] = useState(false);
const enterPressed = useKeyPress("Enter");
const plusPressed = useKeyPress("+");
@@ -77,6 +91,13 @@ const GlobalAddBtn = ({ add }: { add: string }) => {
};
const handleGlobalAddClick = async (e: React.MouseEvent) => {
e.stopPropagation();
+ if (goalToMove) {
+ if (add === "myGoals" || isPartnerModeActive) {
+ navigate(`/goals/${parentId}?addOptions=true`, { state });
+ }
+ return;
+ }
+
if (themeSelection) {
window.history.back();
} else if (add === "myTime" || add === "myGoals" || isPartnerModeActive) {
@@ -102,6 +123,18 @@ const GlobalAddBtn = ({ add }: { add: string }) => {
const { onClick, onMouseDown, onMouseUp, onTouchStart, onTouchEnd } = handlers;
+ const handleMoveGoalHere = async () => {
+ if (!goalToMove) return;
+ setIsDisabled(true);
+ await moveGoalHierarchy(goalToMove.id, parentId).then(() => {
+ setToastMessage({ open: true, message: "Goal moved successfully", extra: "" });
+ });
+ setLastAction("goalMoved");
+ setGoalToMove(null);
+ setIsDisabled(false);
+ window.history.back();
+ };
+
useEffect(() => {
if ((plusPressed || enterPressed) && !state.goalType) {
// @ts-ignore
@@ -109,6 +142,9 @@ const GlobalAddBtn = ({ add }: { add: string }) => {
}
}, [plusPressed, enterPressed]);
+ const shouldRenderMoveButton =
+ goalToMove && goalToMove.id !== parentGoal?.id && goalToMove.parentGoalId !== parentGoal?.id;
+
if (searchParams?.get("addOptions")) {
return (
<>
@@ -119,23 +155,46 @@ const GlobalAddBtn = ({ add }: { add: string }) => {
window.history.back();
}}
/>
- {
- handleAddGoal("Budget");
- }}
- disabled={!isAddingBudgetGoalAllowed}
- bottom={144}
- >
- {t("addBtnBudget")}
-
- {
- handleAddGoal("Standard");
- }}
- bottom={74}
- >
- {t("addBtnGoal")}
-
+ {goalToMove ? (
+ <>
+
+ {t("Move here")}
+
+ {
+ setGoalToMove(null);
+ window.history.back();
+ }}
+ bottom={74}
+ >
+ {t("Cancel")}
+
+ >
+ ) : (
+ <>
+ {
+ handleAddGoal("Budget");
+ }}
+ disabled={!isAddingBudgetGoalAllowed}
+ bottom={144}
+ >
+ {t("addBtnBudget")}
+
+ {
+ handleAddGoal("Standard");
+ }}
+ bottom={74}
+ >
+ {t("addBtnGoal")}
+
+ >
+ )}
>
);
}
diff --git a/src/components/GoalsComponents/DisplayChangesModal/AcceptBtn.tsx b/src/components/GoalsComponents/DisplayChangesModal/AcceptBtn.tsx
index 0b2f87fa5..5e5fe00e4 100644
--- a/src/components/GoalsComponents/DisplayChangesModal/AcceptBtn.tsx
+++ b/src/components/GoalsComponents/DisplayChangesModal/AcceptBtn.tsx
@@ -34,7 +34,9 @@ const AcceptBtn = ({ typeAtPriority, acceptChanges }: AcceptBtnProps) => {
{typeAtPriority === "restored" && "Restore for me too"}
{typeAtPriority === "deleted" && "Delete for me too"}
{typeAtPriority === "subgoals" && "Add all checked"}
+ {typeAtPriority === "newGoalMoved" && "Move for me too"}
{typeAtPriority === "modifiedGoals" && "Make all checked changes"}
+ {typeAtPriority === "moved" && "Move for me too"}
);
};
diff --git a/src/components/GoalsComponents/DisplayChangesModal/DisplayChangesModal.tsx b/src/components/GoalsComponents/DisplayChangesModal/DisplayChangesModal.tsx
index 24df73de1..30faf243a 100644
--- a/src/components/GoalsComponents/DisplayChangesModal/DisplayChangesModal.tsx
+++ b/src/components/GoalsComponents/DisplayChangesModal/DisplayChangesModal.tsx
@@ -19,10 +19,12 @@ import SubHeader from "@src/common/SubHeader";
import ContactItem from "@src/models/ContactItem";
import ZModal from "@src/common/ZModal";
+import { addGoalToNewParentSublist, getAllDescendants, removeGoalFromParentSublist } from "@src/helpers/GoalController";
import Header from "./Header";
import AcceptBtn from "./AcceptBtn";
import IgnoreBtn from "./IgnoreBtn";
import "./DisplayChangesModal.scss";
+import { getMovedSubgoalsList } from "./ShowChanges";
const DisplayChangesModal = ({ currentMainGoal }: { currentMainGoal: GoalItem }) => {
const darkModeStatus = useRecoilValue(darkModeState);
@@ -34,6 +36,33 @@ const DisplayChangesModal = ({ currentMainGoal }: { currentMainGoal: GoalItem })
const [goalUnderReview, setGoalUnderReview] = useState();
const [participants, setParticipants] = useState([]);
const [currentDisplay, setCurrentDisplay] = useState("none");
+ const [oldParentTitle, setOldParentTitle] = useState("");
+ const [newParentTitle, setNewParentTitle] = useState("");
+
+ useEffect(() => {
+ const fetchParentTitles = async () => {
+ if (!goalUnderReview) return;
+
+ try {
+ const currentGoalInDB = await getGoal(goalUnderReview.id);
+ const oldParentId = currentGoalInDB?.parentGoalId;
+
+ const [oldParent, newParent] = await Promise.all([
+ oldParentId ? getGoal(oldParentId) : null,
+ getGoal(goalUnderReview.parentGoalId),
+ ]);
+
+ setOldParentTitle(oldParent?.title || "root");
+ setNewParentTitle(newParent?.title || "Non-shared goal");
+ } catch (error) {
+ console.error("Error fetching parent titles:", error);
+ setOldParentTitle("root");
+ setNewParentTitle("Non-shared goal");
+ }
+ };
+
+ fetchParentTitles();
+ }, [goalUnderReview]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [unselectedChanges, setUnselectedChanges] = useState([]);
@@ -125,13 +154,41 @@ const DisplayChangesModal = ({ currentMainGoal }: { currentMainGoal: GoalItem })
if (!goalUnderReview || !currentMainGoal) {
return;
}
- const removeChanges = currentDisplay === "subgoals" ? newGoals.map(({ goal }) => goal.id) : [goalUnderReview.id];
+ const removeChanges =
+ currentDisplay === "subgoals" || currentDisplay === "newGoalMoved"
+ ? newGoals.map(({ goal }) => goal.id)
+ : currentDisplay === "moved"
+ ? [goalUnderReview.id, ...(await getAllDescendants(goalUnderReview.id)).map((goal: GoalItem) => goal.id)]
+ : [goalUnderReview.id];
+
if (currentDisplay !== "none") {
await deleteGoalChangesInID(currentMainGoal.id, participants[activePPT].relId, currentDisplay, removeChanges);
}
setCurrentDisplay("none");
};
+ const handleMoveChanges = async () => {
+ if (!goalUnderReview) {
+ console.log("No goal under review.");
+ return;
+ }
+ const localGoal = await getGoal(goalUnderReview.id);
+ const localParentGoalId = localGoal?.parentGoalId ?? "root";
+
+ await Promise.all([
+ updateGoal(goalUnderReview.id, { parentGoalId: goalUnderReview.parentGoalId }),
+ removeGoalFromParentSublist(goalUnderReview.id, localParentGoalId),
+ addGoalToNewParentSublist(goalUnderReview.id, goalUnderReview.parentGoalId),
+ ]);
+
+ await sendUpdatedGoal(
+ goalUnderReview.id,
+ [],
+ true,
+ updatesIntent === "suggestion" ? [] : [participants[activePPT].relId],
+ );
+ };
+
const acceptChanges = async () => {
if (!goalUnderReview) {
return;
@@ -139,7 +196,10 @@ const DisplayChangesModal = ({ currentMainGoal }: { currentMainGoal: GoalItem })
if (currentDisplay !== "none") {
await deleteChanges();
}
- if (currentDisplay === "subgoals") {
+ if (currentDisplay === "moved") {
+ await handleMoveChanges();
+ }
+ if (currentDisplay === "subgoals" || currentDisplay === "newGoalMoved") {
const goalsToBeSelected = newGoals
.filter(({ goal }) => !unselectedChanges.includes(goal.id))
.map(({ goal }) => goal);
@@ -205,12 +265,15 @@ const DisplayChangesModal = ({ currentMainGoal }: { currentMainGoal: GoalItem })
console.log("🚀 ~ getChanges ~ changedGoal:", changedGoal);
if (changedGoal) {
setGoalUnderReview({ ...changedGoal });
- if (typeAtPriority === "subgoals") {
+ if (typeAtPriority === "subgoals" || typeAtPriority === "newGoalMoved") {
setNewGoals(goals || []);
} else if (typeAtPriority === "modifiedGoals") {
setUpdatesIntent(goals[0].intent);
const incGoal: GoalItem = { ...goals[0].goal };
setUpdateList({ ...findGoalTagChanges(changedGoal, incGoal) });
+ } else if (typeAtPriority === "moved") {
+ setUpdatesIntent(goals[0].intent);
+ setGoalUnderReview({ ...goals[0].goal });
}
}
}
@@ -284,12 +347,14 @@ const DisplayChangesModal = ({ currentMainGoal }: { currentMainGoal: GoalItem })
contactName={participants[activePPT].name}
title={goalUnderReview.title}
currentDisplay={currentDisplay}
+ newParentTitle={newParentTitle}
/>
)}
{["deleted", "archived", "restored"].includes(currentDisplay) && }
{currentDisplay === "modifiedGoals" && getEditChangesList()}
- {currentDisplay === "subgoals" && getSubgoalsList()}
+ {(currentDisplay === "subgoals" || currentDisplay === "newGoalMoved") && getSubgoalsList()}
+ {currentDisplay === "moved" && getMovedSubgoalsList(goalUnderReview, oldParentTitle, newParentTitle)}
{goalUnderReview && (
diff --git a/src/components/GoalsComponents/DisplayChangesModal/Header.tsx b/src/components/GoalsComponents/DisplayChangesModal/Header.tsx
index 16f5cc644..b71a6f446 100644
--- a/src/components/GoalsComponents/DisplayChangesModal/Header.tsx
+++ b/src/components/GoalsComponents/DisplayChangesModal/Header.tsx
@@ -18,6 +18,12 @@ const Header = ({
{contactName} added to {title}. Add as well ?
>
);
+ case "newGoalMoved":
+ return (
+ <>
+ {contactName} moved new goals to {title}. Move as well ?
+ >
+ );
case "modifiedGoals":
return (
<>
@@ -42,6 +48,12 @@ const Header = ({
{contactName} restored {title}.
>
);
+ case "moved":
+ return (
+ <>
+ {contactName} moved {title}.
+ >
+ );
default:
return <> >;
}
diff --git a/src/components/GoalsComponents/DisplayChangesModal/ShowChanges.scss b/src/components/GoalsComponents/DisplayChangesModal/ShowChanges.scss
new file mode 100644
index 000000000..f2c7e202b
--- /dev/null
+++ b/src/components/GoalsComponents/DisplayChangesModal/ShowChanges.scss
@@ -0,0 +1,35 @@
+.move-info-container {
+ padding: 24px;
+
+ .move-info-item {
+ .move-info-label {
+ color: var(--text-secondary);
+ }
+
+ .move-info-value {
+ color: var(--text-primary);
+ padding: 12px 16px;
+ }
+ }
+
+ .move-direction-container {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ padding: 16px;
+ background: var(--primary-background);
+
+ .arrow {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ margin-top: 24px;
+ }
+ }
+
+ .warning-message {
+ padding: 12px 16px;
+ background: var(--bottom-nav-color);
+ margin-top: 4px;
+ }
+}
diff --git a/src/components/GoalsComponents/DisplayChangesModal/ShowChanges.tsx b/src/components/GoalsComponents/DisplayChangesModal/ShowChanges.tsx
new file mode 100644
index 000000000..512660a0c
--- /dev/null
+++ b/src/components/GoalsComponents/DisplayChangesModal/ShowChanges.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { GoalItem } from "@src/models/GoalItem";
+import "./ShowChanges.scss";
+import { InfoCircleOutlined } from "@ant-design/icons";
+
+export const getMovedSubgoalsList = (
+ goalUnderReview: GoalItem | undefined,
+ oldParentTitle: string,
+ newParentTitle: string,
+) => {
+ if (!goalUnderReview) return null;
+
+ return (
+
+
+
Goal Being Moved
+
{goalUnderReview.title}
+
+
+
+
+
From
+
{oldParentTitle}
+
+
+
→
+
+
+
To
+
{newParentTitle}
+
+
+
+ {newParentTitle === "Non-shared goal" && (
+
+
+ The new parent goal is not shared. The goal will be moved to the root.
+
+ )}
+
+ );
+};
diff --git a/src/components/GoalsComponents/GoalSublist/GoalSublist.tsx b/src/components/GoalsComponents/GoalSublist/GoalSublist.tsx
index 2d53a725b..3d5248be3 100644
--- a/src/components/GoalsComponents/GoalSublist/GoalSublist.tsx
+++ b/src/components/GoalsComponents/GoalSublist/GoalSublist.tsx
@@ -16,6 +16,7 @@ import ArchivedGoals from "@pages/GoalsPage/components/ArchivedGoals";
import { TrashItem } from "@src/models/TrashItem";
import GoalItemSummary from "@src/common/GoalItemSummary/GoalItemSummary";
import AvailableGoalHints from "@pages/GoalsPage/components/AvailableGoalHints";
+import { moveGoalState } from "@src/store/moveGoalState";
import GoalsList from "../GoalsList";
import GoalHistory from "./components/GoalHistory";
import "./GoalSublist.scss";
@@ -34,6 +35,7 @@ export const GoalSublist = () => {
const goalID = useRecoilValue(displayGoalId);
const showChangesModal = useRecoilValue(displayChangesModal);
const showSuggestionModal = useRecoilValue(displaySuggestionsModal);
+ const goalToMove = useRecoilValue(moveGoalState);
useEffect(() => {
if (parentGoal === undefined) return;
@@ -59,7 +61,7 @@ export const GoalSublist = () => {
setActiveGoals([...sortedGoals.filter((goal) => goal.archived === "false")]);
}
init();
- }, [action, parentGoal, showSuggestionModal, showChangesModal, subgoals, goalID]);
+ }, [action, parentGoal, showSuggestionModal, showChangesModal, subgoals, goalID, goalToMove]);
const setGoals = useCallback(
(setGoalsStateUpdateWrapper: (prevGoals: GoalItem[]) => GoalItem[]) => {
diff --git a/src/components/GoalsComponents/MoveGoalButton.tsx b/src/components/GoalsComponents/MoveGoalButton.tsx
new file mode 100644
index 000000000..2859c08d4
--- /dev/null
+++ b/src/components/GoalsComponents/MoveGoalButton.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+import { useRecoilState, useSetRecoilState } from "recoil";
+import { moveGoalHierarchy } from "@src/helpers/GoalController";
+import ZButton from "@src/common/ZButton";
+import { GoalItem } from "@src/models/GoalItem";
+import { lastAction } from "@src/store";
+import { moveGoalState } from "@src/store/moveGoalState";
+import useNavigateToSubgoal from "@src/store/useNavigateToSubgoal";
+
+interface GoalMoveButtonProps {
+ targetGoal: GoalItem;
+}
+
+const GoalMoveButton: React.FC
= ({ targetGoal }) => {
+ const navigateToSubgoal = useNavigateToSubgoal();
+ const [selectedGoal, setSelectedGoal] = useRecoilState(moveGoalState);
+ const setLastAction = useSetRecoilState(lastAction);
+
+ const handleClick = () => {
+ if (selectedGoal && targetGoal?.id) {
+ moveGoalHierarchy(selectedGoal.id, targetGoal.id)
+ .then(() => {
+ setLastAction("goalMoved");
+ })
+ .then(() => {
+ navigateToSubgoal(targetGoal);
+ })
+ .finally(() => {
+ setSelectedGoal(null);
+ });
+ }
+ };
+
+ return (
+
+ Move Here
+
+ );
+};
+
+export default GoalMoveButton;
diff --git a/src/components/GoalsComponents/MyGoal/MyGoal.tsx b/src/components/GoalsComponents/MyGoal/MyGoal.tsx
index f4add9bfd..a6b120bd8 100644
--- a/src/components/GoalsComponents/MyGoal/MyGoal.tsx
+++ b/src/components/GoalsComponents/MyGoal/MyGoal.tsx
@@ -8,6 +8,7 @@ import { ILocationState, ImpossibleGoal } from "@src/Interfaces";
import { useParentGoalContext } from "@src/contexts/parentGoal-context";
import { extractLinks, isGoalCode } from "@src/utils/patterns";
import useGoalActions from "@src/hooks/useGoalActions";
+import { moveGoalState } from "@src/store/moveGoalState";
import GoalAvatar from "../GoalAvatar";
import GoalTitle from "./components/GoalTitle";
import GoalDropdown from "./components/GoalDropdown";
@@ -22,6 +23,8 @@ const MyGoal: React.FC = ({ goal, dragAttributes, dragListeners })
const { partnerId } = useParams();
const isPartnerModeActive = !!partnerId;
+ const goalToMove = useRecoilValue(moveGoalState);
+
const [expandGoalId, setExpandGoalId] = useState("root");
const [isAnimating, setIsAnimating] = useState(true);
const { copyCode } = useGoalActions();
@@ -91,7 +94,7 @@ const MyGoal: React.FC = ({ goal, dragAttributes, dragListeners })
key={String(`goal-${goal.id}`)}
className={`user-goal${darkModeStatus ? "-dark" : ""} ${
expandGoalId === goal.id && isAnimating ? "goal-glow" : ""
- }`}
+ } ${goalToMove && goalToMove.id === goal.id ? "goal-to-move-selected" : ""}`}
>
{
const navigate = useNavigate();
const { t } = useTranslation();
const { partnerId } = useParams();
- const { openEditMode } = useGoalStore();
+ const { openEditMode, handleMove } = useGoalStore();
const { state, pathname }: { state: ILocationState; pathname: string } = useLocation();
const { deleteGoalAction } = useGoalActions();
const isPartnerModeActive = !!partnerId;
@@ -74,6 +74,8 @@ const RegularGoalActions = ({ goal }: { goal: GoalItem }) => {
} else if (action === "colabRequest") {
await convertSharedWMGoalToColab(goal);
setLastAction("goalColabRequest");
+ } else if (action === "move") {
+ await handleMove(goal);
}
window.history.back();
};
@@ -159,6 +161,17 @@ const RegularGoalActions = ({ goal }: { goal: GoalItem }) => {
>
+ {!isPartnerModeActive && (
+ {
+ e.stopPropagation();
+ await openConfirmationPopUp({ actionCategory: "goal", actionName: "move" });
+ }}
+ >
+
+
+ )}
);
diff --git a/src/components/index.scss b/src/components/index.scss
index 6fefe4aea..d5ae8e788 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -38,7 +38,7 @@
align-items: center;
background-color: var(--secondary-background);
padding: 5px;
- width: 150px;
+ width: 180px;
right: 35px;
&.disabled {
diff --git a/src/constants/goals.ts b/src/constants/goals.ts
index a610ee1af..f2299716a 100644
--- a/src/constants/goals.ts
+++ b/src/constants/goals.ts
@@ -62,6 +62,9 @@ export const getConfirmButtonText = (actionName: string) => {
case "reportHint":
confirmButtonText = "reportHint";
break;
+ case "move":
+ confirmButtonText = "moveGoal";
+ break;
default:
}
return confirmButtonText;
diff --git a/src/helpers/GoalController.ts b/src/helpers/GoalController.ts
index eca44850a..f625c8e0a 100644
--- a/src/helpers/GoalController.ts
+++ b/src/helpers/GoalController.ts
@@ -1,5 +1,6 @@
-import { GoalItem } from "@src/models/GoalItem";
-import { getSelectedLanguage, inheritParentProps } from "@src/utils";
+/* eslint-disable no-await-in-loop */
+import { GoalItem, IParticipant } from "@src/models/GoalItem";
+import { inheritParentProps } from "@src/utils";
import { sendUpdatesToSubscriber } from "@src/services/contact.service";
import { getSharedWMGoal, removeSharedWMChildrenGoals, updateSharedWMGoal } from "@src/api/SharedWMAPI";
import {
@@ -10,6 +11,7 @@ import {
removeGoalWithChildrens,
getParticipantsOfGoals,
getHintsFromAPI,
+ getChildrenGoals,
} from "@src/api/GoalsAPI";
import { addHintItem, updateHintItem } from "@src/api/HintsAPI";
import { restoreUserGoal } from "@src/api/TrashAPI";
@@ -17,36 +19,196 @@ import { sendFinalUpdateOnGoal, sendUpdatedGoal } from "./PubSubController";
export const createGoal = async (newGoal: GoalItem, parentGoalId: string, ancestors: string[], hintOption: boolean) => {
const level = ancestors.length;
+
if (hintOption) {
getHintsFromAPI(newGoal)
- .then((hints) => addHintItem(newGoal.id, hintOption, hints || []))
+ .then((hints) => {
+ addHintItem(newGoal.id, hintOption, hints || []);
+ })
.catch((error) => console.error("Error fetching hints:", error));
}
if (parentGoalId && parentGoalId !== "root") {
const parentGoal = await getGoal(parentGoalId);
- if (!parentGoal) return { parentGoal: null };
- const newGoalId = await addGoal(inheritParentProps(newGoal, parentGoal));
+ if (!parentGoal) {
+ console.warn("Parent goal not found:", parentGoalId);
+ return { parentGoal: null };
+ }
+
+ const ancestorGoals = await Promise.all(ancestors.map((id) => getGoal(id)));
+ const allParticipants = new Map();
+
+ [...ancestorGoals, parentGoal].forEach((goal) => {
+ if (!goal?.participants) return;
+ goal.participants.forEach((participant) => {
+ if (participant.following) {
+ allParticipants.set(participant.relId, participant);
+ }
+ });
+ });
+ const goalWithParentProps = inheritParentProps(newGoal, parentGoal);
+ const updatedGoal = {
+ ...goalWithParentProps,
+ participants: Array.from(allParticipants.values()),
+ };
+
+ const newGoalId = await addGoal(updatedGoal);
- const subscribers = await getParticipantsOfGoals(ancestors);
if (newGoalId) {
- subscribers.map(async ({ sub, rootGoalId }) => {
- sendUpdatesToSubscriber(sub, rootGoalId, "subgoals", [
- {
- level,
- goal: { ...newGoal, id: newGoalId },
+ const subscriberUpdates = new Map<
+ string,
+ {
+ sub: IParticipant;
+ rootGoalId: string;
+ updates: Array<{ level: number; goal: Omit }>;
+ }
+ >();
+
+ const subscribers = await getParticipantsOfGoals(ancestors);
+
+ const uniqueSubscribers = new Map();
+
+ subscribers.forEach(({ sub, rootGoalId }) => {
+ if (!uniqueSubscribers.has(sub.relId)) {
+ uniqueSubscribers.set(sub.relId, { sub, rootGoalId });
+ }
+ });
+
+ uniqueSubscribers.forEach(({ sub, rootGoalId }) => {
+ if (!subscriberUpdates.has(sub.relId)) {
+ subscriberUpdates.set(sub.relId, { sub, rootGoalId, updates: [] });
+ }
+ const subscriberUpdate = subscriberUpdates.get(sub.relId);
+ subscriberUpdate?.updates.push({
+ level,
+ goal: {
+ ...updatedGoal,
+ id: newGoalId,
+ rootGoalId,
},
- ]).then(() => console.log("update sent"));
+ });
});
+
+ await Promise.all(
+ Array.from(subscriberUpdates.values()).map(({ sub, rootGoalId, updates }) =>
+ sendUpdatesToSubscriber(sub, rootGoalId, "subgoals", updates),
+ ),
+ );
}
+
const newSublist = parentGoal && parentGoal.sublist ? [...parentGoal.sublist, newGoalId] : [newGoalId];
await updateGoal(parentGoalId, { sublist: newSublist });
return { parentGoal };
}
+
await addGoal(newGoal);
return { parentGoal: null };
};
+export const getGoalAncestors = async (goalId: string): Promise => {
+ const ancestors: string[] = [];
+ let currentGoalId = goalId;
+
+ while (currentGoalId !== "root") {
+ const currentGoal = await getGoal(currentGoalId);
+ if (!currentGoal || currentGoal.parentGoalId === "root") break;
+
+ ancestors.unshift(currentGoal.parentGoalId);
+ currentGoalId = currentGoal.parentGoalId;
+ }
+
+ return ancestors;
+};
+
+export const getAllDescendants = async (goalId: string): Promise => {
+ const descendants: GoalItem[] = [];
+
+ const processGoalAndChildren = async (currentGoalId: string) => {
+ const childrenGoals = await getChildrenGoals(currentGoalId);
+ await Promise.all(
+ childrenGoals.map(async (childGoal) => {
+ descendants.push(childGoal);
+ await processGoalAndChildren(childGoal.id);
+ }),
+ );
+ };
+
+ await processGoalAndChildren(goalId);
+ return descendants;
+};
+
+export const createSharedGoalObjectForSending = async (
+ newGoal: GoalItem,
+ parentGoalId: string,
+ ancestors: string[],
+) => {
+ const level = ancestors.length;
+
+ if (parentGoalId && parentGoalId !== "root") {
+ const parentGoal = await getGoal(parentGoalId);
+ if (!parentGoal) {
+ return { parentGoal: null };
+ }
+
+ const newGoalId = newGoal.id;
+ const subscribers = await getParticipantsOfGoals(ancestors);
+ const descendants = await getAllDescendants(newGoalId);
+
+ if (newGoalId) {
+ try {
+ const subscriberUpdates = new Map<
+ string,
+ {
+ sub: IParticipant;
+ rootGoalId: string;
+ updates: Array<{ level: number; goal: Omit }>;
+ }
+ >();
+
+ subscribers.forEach(({ sub, rootGoalId }) => {
+ subscriberUpdates.set(sub.relId, {
+ sub,
+ rootGoalId,
+ updates: [
+ {
+ level,
+ goal: { ...newGoal, id: newGoalId, parentGoalId },
+ },
+ ],
+ });
+ });
+
+ if (descendants.length > 0) {
+ subscribers.forEach(({ sub, rootGoalId }) => {
+ const subscriberUpdate = subscriberUpdates.get(sub.relId);
+ if (subscriberUpdate) {
+ subscriberUpdate.updates.push(
+ ...descendants.map((descendant) => ({
+ level: level + 1,
+ goal: {
+ ...descendant,
+ rootGoalId,
+ },
+ })),
+ );
+ }
+ });
+ }
+
+ await Promise.all(
+ Array.from(subscriberUpdates.values()).map(({ sub, rootGoalId, updates }) =>
+ sendUpdatesToSubscriber(sub, rootGoalId, "newGoalMoved", updates),
+ ),
+ );
+ } catch (error) {
+ console.error("[createSharedGoal] Error sending updates:", error);
+ }
+ }
+ return { parentGoal };
+ }
+ return { parentGoal: null };
+};
+
export const modifyGoal = async (
goalId: string,
goalTags: GoalItem,
@@ -100,3 +262,165 @@ export const deleteSharedGoal = async (goal: GoalItem) => {
});
}
};
+
+const updateRootGoal = async (goalId: string, newRootGoalId: string) => {
+ await updateGoal(goalId, { rootGoalId: newRootGoalId });
+
+ const childrenGoals = await getChildrenGoals(goalId);
+ if (childrenGoals) {
+ childrenGoals.forEach(async (goal: GoalItem) => {
+ await updateRootGoal(goal.id, newRootGoalId);
+ });
+ }
+};
+
+export const getRootGoalId = async (goalId: string): Promise => {
+ const goal = await getGoal(goalId);
+ if (!goal || goal.parentGoalId === "root") {
+ return goal?.id || "root";
+ }
+ return getRootGoalId(goal.parentGoalId);
+};
+
+export const updateRootGoalNotification = async (goalId: string) => {
+ const rootGoalId = await getRootGoalId(goalId);
+ if (rootGoalId !== "root") {
+ await updateGoal(rootGoalId, { newUpdates: true });
+ }
+};
+
+export const removeGoalFromParentSublist = async (goalId: string, parentGoalId: string) => {
+ const parentGoal = await getGoal(parentGoalId);
+ if (!parentGoal) return;
+
+ const parentGoalSublist = parentGoal.sublist;
+ const childGoalIndex = parentGoalSublist.indexOf(goalId);
+ if (childGoalIndex !== -1) {
+ parentGoalSublist.splice(childGoalIndex, 1);
+ }
+ await updateGoal(parentGoal.id, { sublist: parentGoalSublist });
+};
+
+export const addGoalToNewParentSublist = async (goalId: string, newParentGoalId: string) => {
+ const newParentGoal = await getGoal(newParentGoalId);
+ if (!newParentGoal) return;
+
+ const isGoalAlreadyInSublist = newParentGoal.sublist.includes(goalId);
+ if (isGoalAlreadyInSublist) return;
+
+ const newParentGoalSublist = newParentGoal.sublist;
+ newParentGoalSublist.push(goalId);
+ await updateGoal(newParentGoal.id, { sublist: newParentGoalSublist });
+};
+
+export const getGoalHistoryToRoot = async (goalId: string): Promise<{ goalID: string; title: string }[]> => {
+ const history: { goalID: string; title: string }[] = [];
+ let currentGoalId = goalId;
+
+ while (currentGoalId !== "root") {
+ const currentGoal = await getGoal(currentGoalId);
+
+ if (!currentGoal) {
+ break;
+ }
+
+ history.unshift({ goalID: currentGoal.id, title: currentGoal.title });
+ currentGoalId = currentGoal.parentGoalId;
+ }
+
+ return history;
+};
+
+export const moveGoalHierarchy = async (goalId: string, newParentGoalId: string) => {
+ const goalToMove = await getGoal(goalId);
+ const newParentGoal = await getGoal(newParentGoalId);
+
+ if (!goalToMove) {
+ return;
+ }
+
+ const oldParentId = goalToMove.parentGoalId;
+ const ancestors = await getGoalHistoryToRoot(goalId);
+ const ancestorGoalIds = ancestors.map((ele) => ele.goalID);
+
+ const ancestorGoalsOfNewParent = await getGoalHistoryToRoot(newParentGoalId);
+ const ancestorGoalIdsOfNewParent = ancestorGoalsOfNewParent.map((ele) => ele.goalID);
+
+ const ancestorGoals = await Promise.all(ancestorGoalIdsOfNewParent.map((id) => getGoal(id)));
+ const allParticipants = new Map();
+
+ [...ancestorGoals, newParentGoal].forEach((goal) => {
+ if (!goal?.participants) return;
+ goal.participants.forEach((participant) => {
+ if (participant.following) {
+ allParticipants.set(participant.relId, participant);
+ }
+ });
+ });
+
+ goalToMove.participants.forEach((participant) => {
+ if (participant.following) {
+ allParticipants.set(participant.relId, participant);
+ }
+ });
+
+ const updatedGoal = {
+ ...goalToMove,
+ participants: Array.from(allParticipants.values()),
+ };
+
+ const isNonSharedGoal = !goalToMove?.participants?.some((p) => p.following);
+
+ if (isNonSharedGoal) {
+ await createSharedGoalObjectForSending(updatedGoal, newParentGoalId, ancestorGoalIdsOfNewParent);
+ } else {
+ const subscribers = await getParticipantsOfGoals(ancestorGoalIds);
+
+ try {
+ await Promise.all(
+ subscribers.map(({ sub, rootGoalId }) =>
+ sendUpdatesToSubscriber(sub, rootGoalId, "moved", [
+ {
+ level: ancestorGoalIds.length,
+ goal: {
+ ...updatedGoal,
+ parentGoalId: newParentGoalId,
+ rootGoalId,
+ },
+ },
+ ]),
+ ),
+ );
+ } catch (error) {
+ console.error("[moveGoalHierarchy] Error sending move updates:", error);
+ }
+ }
+
+ try {
+ await Promise.all([
+ updateGoal(goalToMove.id, {
+ parentGoalId: newParentGoalId,
+ participants: updatedGoal.participants,
+ }),
+ removeGoalFromParentSublist(goalToMove.id, oldParentId),
+ addGoalToNewParentSublist(goalToMove.id, newParentGoalId),
+ updateRootGoal(goalToMove.id, newParentGoal?.rootGoalId ?? "root"),
+ ]);
+
+ const descendants = await getAllDescendants(goalId);
+ if (descendants.length > 0) {
+ await Promise.all(
+ descendants.map((descendant) =>
+ updateGoal(descendant.id, {
+ participants: updatedGoal.participants,
+ rootGoalId: newParentGoal?.rootGoalId ?? "root",
+ }),
+ ),
+ );
+ }
+ } catch (error) {
+ console.error("[moveGoalHierarchy] Error updating goal relationships:", error);
+ }
+
+ console.log("[moveGoalHierarchy] Successfully completed goal hierarchy move");
+};
diff --git a/src/helpers/GoalProcessor.ts b/src/helpers/GoalProcessor.ts
index 228cb023f..85ef2e26a 100644
--- a/src/helpers/GoalProcessor.ts
+++ b/src/helpers/GoalProcessor.ts
@@ -112,6 +112,10 @@ export const getTypeAtPriority = (goalChanges: IChangesInGoal) => {
typeAtPriority = "deleted";
} else if (goalChanges.restored.length > 0) {
typeAtPriority = "restored";
+ } else if (goalChanges.moved.length > 0) {
+ typeAtPriority = "moved";
+ } else if (goalChanges.newGoalMoved.length > 0) {
+ typeAtPriority = "newGoalMoved";
}
return { typeAtPriority };
};
@@ -129,22 +133,23 @@ export const jumpToLowestChanges = async (id: string, relId: string) => {
const parentId =
"id" in goalAtPriority
? goalAtPriority.id
- : typeAtPriority === "subgoals"
+ : typeAtPriority === "subgoals" || typeAtPriority === "newGoalMoved"
? goalAtPriority.goal.parentGoalId
: goalAtPriority.goal.id;
if (typeAtPriority === "archived" || typeAtPriority === "deleted") {
- return { typeAtPriority, parentId, goals: [await getGoal(parentId)] };
+ const result = { typeAtPriority, parentId, goals: [await getGoal(parentId)] };
+ return result;
}
- if (typeAtPriority === "subgoals") {
- goalChanges.subgoals.forEach(({ intent, goal }) => {
+ if (typeAtPriority === "subgoals" || typeAtPriority === "newGoalMoved") {
+ goalChanges[typeAtPriority].forEach(({ intent, goal }) => {
if (goal.parentGoalId === parentId) goals.push({ intent, goal });
});
}
- if (typeAtPriority === "modifiedGoals") {
+ if (typeAtPriority === "modifiedGoals" || typeAtPriority === "moved") {
let modifiedGoal = createGoalObjectFromTags({});
let goalIntent;
- goalChanges.modifiedGoals.forEach(({ goal, intent }) => {
+ goalChanges[typeAtPriority].forEach(({ goal, intent }) => {
if (goal.id === parentId) {
modifiedGoal = { ...modifiedGoal, ...goal };
goalIntent = intent;
@@ -153,16 +158,16 @@ export const jumpToLowestChanges = async (id: string, relId: string) => {
goals = [{ intent: goalIntent, goal: modifiedGoal }];
}
- return {
+ const result = {
typeAtPriority,
parentId,
goals,
};
+ return result;
}
- } else {
- console.log("inbox item doesn't exist");
}
- return { typeAtPriority, parentId: "", goals: [] };
+ const defaultResult = { typeAtPriority, parentId: "", goals: [] };
+ return defaultResult;
};
export const findGoalTagChanges = (goal1: GoalItem, goal2: GoalItem) => {
diff --git a/src/helpers/InboxProcessor.ts b/src/helpers/InboxProcessor.ts
index 4536bd700..33968160a 100644
--- a/src/helpers/InboxProcessor.ts
+++ b/src/helpers/InboxProcessor.ts
@@ -1,5 +1,12 @@
-import { GoalItem } from "@src/models/GoalItem";
-import { changesInGoal, InboxItem } from "@src/models/InboxItem";
+import { GoalItem, typeOfSub } from "@src/models/GoalItem";
+import {
+ ChangesByType,
+ changesInGoal,
+ changesInId,
+ IdChangeTypes,
+ typeOfChange,
+ typeOfIntent,
+} from "@src/models/InboxItem";
import { getDefaultValueOfGoalChanges } from "@src/utils/defaultGenerators";
import {
@@ -17,13 +24,123 @@ import {
removeSharedWMChildrenGoals,
removeSharedWMGoal,
updateSharedWMGoal,
+ updateSharedWMParentSublist,
} from "@src/api/SharedWMAPI";
import { ITagChangesSchemaVersion, ITagsChanges } from "@src/Interfaces/IDisplayChangesModal";
import { fixDateVlauesInGoalObject } from "@src/utils";
import { getDeletedGoal, restoreUserGoal } from "@src/api/TrashAPI";
+import { getContactByRelId } from "@src/api/ContactsAPI";
+import { getAllDescendants, getRootGoalId, updateRootGoalNotification } from "./GoalController";
+import { isIncomingGoalLatest, isIncomingIdChangeLatest } from "./mergeSharedGoalItems";
+
+export interface Payload {
+ relId: string;
+ lastProcessedTimestamp: string;
+ changeType: typeOfChange;
+ rootGoalId: string;
+ changes: (changesInGoal | changesInId)[];
+ type: string;
+ timestamp: string;
+ TTL: number;
+}
+
+const isIdChangeType = (type: typeOfChange): type is IdChangeTypes => {
+ return ["deleted", "moved", "restored", "archived"].includes(type);
+};
+
+const updateSharedWMGoalAndDescendants = async (movedGoal: GoalItem) => {
+ await updateSharedWMGoal(movedGoal.id, {
+ parentGoalId: movedGoal.parentGoalId,
+ rootGoalId: movedGoal.rootGoalId,
+ });
+
+ const descendants = await getAllDescendants(movedGoal.id);
+ if (descendants.length > 0) {
+ await Promise.all(
+ descendants.map((descendant) =>
+ updateSharedWMGoal(descendant.id, {
+ rootGoalId: movedGoal.rootGoalId,
+ }),
+ ),
+ );
+ }
+};
+
+const handleMoveOperation = async (movedGoal: GoalItem, correspondingSharedWMGoal: GoalItem) => {
+ const isNewParentAvailable = await getSharedWMGoal(movedGoal.parentGoalId);
+ const updatedGoal = {
+ ...movedGoal,
+ parentGoalId: !isNewParentAvailable ? "root" : movedGoal.parentGoalId,
+ rootGoalId: !isNewParentAvailable ? "root" : movedGoal.rootGoalId,
+ };
+
+ await updateSharedWMParentSublist(
+ correspondingSharedWMGoal.parentGoalId,
+ updatedGoal.parentGoalId,
+ correspondingSharedWMGoal.id,
+ );
+ await updateSharedWMGoalAndDescendants(updatedGoal);
+};
+
+const addChangesToRootGoal = async (goalId: string, relId: string, changes: ChangesByType) => {
+ const rootGoalId = await getRootGoalId(goalId);
+ if (rootGoalId === "root") return;
+
+ const inbox = await getInboxItem(rootGoalId);
+ if (!inbox) {
+ await createEmptyInboxItem(rootGoalId);
+ }
+
+ await Promise.all([updateRootGoalNotification(rootGoalId), addGoalChangesInID(rootGoalId, relId, changes)]);
+};
+
+const handleSubgoalsChanges = async (
+ changes: changesInGoal[],
+ changeType: typeOfChange,
+ rootGoalId: string,
+ relId: string,
+ type: string,
+) => {
+ const rootGoal = await getGoal(rootGoalId);
+ if (!rootGoal) {
+ console.warn("[handleSubgoalsChanges] Root goal not found:", rootGoalId);
+ return;
+ }
+
+ if (!rootGoal.participants.find((p) => p.relId === relId && p.following)) {
+ console.warn("[handleSubgoalsChanges] Participant not found or not following:", relId);
+ return;
+ }
+
+ const contact = await getContactByRelId(relId);
+
+ const goalsWithParticipants = changes.map((ele: changesInGoal) => ({
+ ...ele,
+ goal: {
+ ...fixDateVlauesInGoalObject(ele.goal),
+ participants: [{ relId, following: true, type: "sharer" as typeOfSub, name: contact?.name || "" }],
+ },
+ }));
+
+ const inbox = await getInboxItem(rootGoalId);
+ if (!inbox) {
+ await createEmptyInboxItem(rootGoalId);
+ }
+
+ const defaultChanges = getDefaultValueOfGoalChanges();
+ (defaultChanges[changeType] as changesInGoal[]) = goalsWithParticipants.map((ele) => ({
+ ...ele,
+ intent: type as typeOfIntent,
+ }));
+
+ await addChangesToRootGoal(rootGoalId, relId, defaultChanges);
+};
+
+export const handleIncomingChanges = async (payload: Payload, relId: string) => {
+ console.log("Incoming change", payload);
-export const handleIncomingChanges = async (payload, relId) => {
if (payload.type === "sharer" && (await getSharedWMGoal(payload.rootGoalId))) {
+ console.log("Incoming change is a shared goal. Processing...");
const incGoal = await getSharedWMGoal(payload.rootGoalId);
if (!incGoal || incGoal.participants.find((ele) => ele.relId === relId && ele.following) === undefined) {
console.log("Changes ignored");
@@ -33,14 +150,23 @@ export const handleIncomingChanges = async (payload, relId) => {
const changes = [
...payload.changes.map((ele: changesInGoal) => ({ ...ele, goal: fixDateVlauesInGoalObject(ele.goal) })),
];
- await addGoalsInSharedWM([changes[0].goal]);
+ await addGoalsInSharedWM([changes[0].goal], relId);
+ } else if (payload.changeType === "newGoalMoved") {
+ const changes = [
+ ...payload.changes.map((ele: changesInGoal) => ({ ...ele, goal: fixDateVlauesInGoalObject(ele.goal) })),
+ ];
+ changes.map(async (ele) => {
+ await addGoalsInSharedWM([ele.goal], relId);
+ });
} else if (payload.changeType === "modifiedGoals") {
const changes = [
...payload.changes.map((ele: changesInGoal) => ({ ...ele, goal: fixDateVlauesInGoalObject(ele.goal) })),
];
await updateSharedWMGoal(changes[0].goal.id, changes[0].goal);
} else if (payload.changeType === "deleted") {
- const goalToBeDeleted = await getSharedWMGoal(payload.changes[0].id);
+ const change = payload.changes[0] as changesInId;
+ const goalToBeDeleted = await getSharedWMGoal(change.id);
+ console.log("Deleting goal", goalToBeDeleted);
await removeSharedWMChildrenGoals(goalToBeDeleted.id);
await removeSharedWMGoal(goalToBeDeleted);
if (goalToBeDeleted.parentGoalId !== "root") {
@@ -54,44 +180,106 @@ export const handleIncomingChanges = async (payload, relId) => {
});
}
} else if (payload.changeType === "archived") {
- getSharedWMGoal(payload.changes[0].id).then(async (goal: GoalItem) =>
+ const change = payload.changes[0] as changesInId;
+ getSharedWMGoal(change.id).then(async (goal: GoalItem) =>
archiveSharedWMGoal(goal).catch((err) => console.log(err, "failed to archive")),
);
} else if (payload.changeType === "restored") {
- const goalToBeRestored = await getDeletedGoal(payload.changes[0].id);
+ const change = payload.changes[0] as changesInId;
+ const goalToBeRestored = await getDeletedGoal(change.id);
if (goalToBeRestored) {
await restoreUserGoal(goalToBeRestored, true);
}
+ } else if (payload.changeType === "moved") {
+ const changes = [
+ ...payload.changes.map((ele: changesInGoal) => ({ ...ele, goal: fixDateVlauesInGoalObject(ele.goal) })),
+ ];
+ const movedGoal = changes[0].goal;
+ const correspondingSharedWMGoal = await getSharedWMGoal(movedGoal.id);
+
+ if (!correspondingSharedWMGoal) {
+ console.log("Goal to move not found");
+ return;
+ }
+
+ await handleMoveOperation(movedGoal, correspondingSharedWMGoal);
}
} else if (["sharer", "suggestion"].includes(payload.type)) {
- const { rootGoalId, changes, changeType } = payload;
- const rootGoal = await getGoal(rootGoalId);
- if (rootGoal) {
- let inbox: InboxItem = await getInboxItem(rootGoalId);
- const defaultChanges = getDefaultValueOfGoalChanges();
- defaultChanges[changeType] = [...changes.map((ele) => ({ ...ele, intent: payload.type }))];
- if (!inbox) {
- await createEmptyInboxItem(rootGoalId);
- inbox = await getInboxItem(rootGoalId);
- }
- await Promise.all([
- updateGoal(rootGoalId, { newUpdates: true }),
- await addGoalChangesInID(rootGoalId, relId, defaultChanges),
- ]);
+ const { changes, changeType, rootGoalId } = payload;
+ if (changeType === "subgoals" || changeType === "newGoalMoved") {
+ await handleSubgoalsChanges(changes as changesInGoal[], changeType, rootGoalId, relId, payload.type);
+ return;
+ }
+
+ const goalId = "goal" in changes[0] ? changes[0].goal.id : changes[0].id;
+ const localGoal = await getGoal(goalId);
+
+ if (!localGoal || !localGoal.participants.find((p) => p.relId === relId && p.following)) {
+ console.log("Goal not found or not shared with participant");
+ return;
+ }
+
+ const allAreLatest = await Promise.all(
+ changes.map(async (ele) => {
+ let isLatest = true;
+ if ("goal" in ele) {
+ isLatest = await isIncomingGoalLatest(localGoal.id, ele.goal);
+ } else {
+ isLatest = await isIncomingIdChangeLatest(localGoal.id, ele.timestamp);
+ }
+ return isLatest ? ele : null;
+ }),
+ );
+
+ const filteredChanges = allAreLatest.filter((ele) => ele !== null);
+
+ if (filteredChanges.length === 0) {
+ console.log("All changes are not latest");
+ return;
}
+
+ const inbox = await getInboxItem(localGoal.id);
+ const defaultChanges = getDefaultValueOfGoalChanges();
+ if (isIdChangeType(changeType)) {
+ defaultChanges[changeType] = filteredChanges.map((ele) => ({
+ ...(ele as changesInId),
+ intent: payload.type as typeOfIntent,
+ }));
+ } else {
+ defaultChanges[changeType] = filteredChanges.map((ele) => ({
+ ...(ele as changesInGoal),
+ intent: payload.type as typeOfIntent,
+ }));
+ }
+
+ if (!inbox) {
+ await createEmptyInboxItem(localGoal.id);
+ }
+
+ await addChangesToRootGoal(localGoal.id, relId, defaultChanges);
}
};
export const acceptSelectedSubgoals = async (selectedGoals: GoalItem[], parentGoal: GoalItem) => {
try {
const childrens: string[] = [];
+
selectedGoals.forEach(async (goal: GoalItem) => {
- addGoal(fixDateVlauesInGoalObject({ ...goal, participants: [] })).catch((err) => console.log(err));
+ const { relId } = goal.participants[0];
+ const contact = await getContactByRelId(relId);
+ addGoal(
+ fixDateVlauesInGoalObject({
+ ...goal,
+ participants: [{ relId, following: true, type: "sharer", name: contact?.name || "" }],
+ }),
+ ).catch((err) => console.log(err));
childrens.push(goal.id);
});
await addIntoSublist(parentGoal.id, childrens);
+
return "Done!!";
} catch (err) {
+ console.error("[acceptSelectedSubgoals] Failed to add changes:", err);
return "Failed To add Changes";
}
};
diff --git a/src/helpers/PartnerController.ts b/src/helpers/PartnerController.ts
index 29ff5953b..5857d66ef 100644
--- a/src/helpers/PartnerController.ts
+++ b/src/helpers/PartnerController.ts
@@ -8,7 +8,7 @@ import { createGoalObjectFromTags } from "./GoalProcessor";
const sendUpdate = (
subscribers: IParticipant[],
rootGoalId: string,
- type: "subgoals" | "modifiedGoals",
+ type: "subgoals" | "modifiedGoals" | "newGoalMoved",
obj: {
level: number;
goal: GoalItem;
diff --git a/src/helpers/PubSubController.ts b/src/helpers/PubSubController.ts
index a1c7ca09f..2b955ed80 100644
--- a/src/helpers/PubSubController.ts
+++ b/src/helpers/PubSubController.ts
@@ -22,7 +22,7 @@ export const sendUpdatedGoal = async (
.filter((ele) => !excludeSubs.includes(ele.sub.relId))
.forEach(async ({ sub, rootGoalId }) => {
sendUpdatesToSubscriber(sub, rootGoalId, "modifiedGoals", [
- { level: ancestorGoalIds.length, goal: { ...changes, rootGoalId } },
+ { level: ancestorGoalIds.length, goal: { ...changes, rootGoalId, timestamp: Date.now() } },
]).then(() => console.log("update sent"));
});
}
@@ -30,23 +30,35 @@ export const sendUpdatedGoal = async (
export const sendFinalUpdateOnGoal = async (
goalId: string,
- action: "archived" | "deleted" | "restored",
+ action: "archived" | "deleted" | "restored" | "moved",
ancestors: string[] = [],
redefineAncestors = true,
excludeSubs: string[] = [],
+ timestamp: number = Date.now(),
) => {
+ console.log(`[sendFinalUpdateOnGoal] Starting for goalId: ${goalId}, action: ${action}`);
+
const ancestorGoalIds = redefineAncestors ? (await getHistoryUptoGoal(goalId)).map((ele) => ele.goalID) : ancestors;
+ console.log("[sendFinalUpdateOnGoal] Ancestor IDs:", ancestorGoalIds);
+
const subscribers = await getParticipantsOfGoals(ancestorGoalIds);
+ console.log("[sendFinalUpdateOnGoal] Initial subscribers:", subscribers.length);
+
if (action === "restored") {
- (await getParticipantsOfDeletedGoal(goalId)).forEach((doc) => {
+ const deletedGoalParticipants = await getParticipantsOfDeletedGoal(goalId);
+ console.log("[sendFinalUpdateOnGoal] Additional restored participants:", deletedGoalParticipants.length);
+ deletedGoalParticipants.forEach((doc) => {
subscribers.push(doc);
});
}
- subscribers
- .filter((ele) => !excludeSubs.includes(ele.sub.relId))
- .forEach(async ({ sub, rootGoalId }) => {
- sendUpdatesToSubscriber(sub, rootGoalId, action, [{ level: ancestorGoalIds.length, id: goalId }]).then(() =>
- console.log("update sent"),
- );
- });
+
+ const filteredSubscribers = subscribers.filter((ele) => !excludeSubs.includes(ele.sub.relId));
+ console.log("[sendFinalUpdateOnGoal] Filtered subscribers:", filteredSubscribers.length);
+
+ filteredSubscribers.forEach(async ({ sub, rootGoalId }) => {
+ console.log(`[sendFinalUpdateOnGoal] Sending update to subscriber ${sub.relId} for root goal ${rootGoalId}`);
+ sendUpdatesToSubscriber(sub, rootGoalId, action, [{ level: ancestorGoalIds.length, id: goalId, timestamp }])
+ .then(() => console.log(`[sendFinalUpdateOnGoal] Update sent successfully to ${sub.relId}`))
+ .catch((error) => console.error(`[sendFinalUpdateOnGoal] Error sending update to ${sub.relId}:`, error));
+ });
};
diff --git a/src/helpers/mergeSharedGoalItems.ts b/src/helpers/mergeSharedGoalItems.ts
new file mode 100644
index 000000000..7f55824f8
--- /dev/null
+++ b/src/helpers/mergeSharedGoalItems.ts
@@ -0,0 +1,27 @@
+import { getGoalById } from "@src/api/GoalsAPI";
+import { GoalItem } from "@src/models/GoalItem";
+import { changesInId } from "@src/models/InboxItem";
+
+export async function isIncomingGoalLatest(localGoalId: string, incomingGoal: GoalItem): Promise {
+ const localGoal = await getGoalById(localGoalId);
+
+ if (!localGoal) {
+ return true;
+ }
+
+ if (incomingGoal.timestamp > localGoal.timestamp) {
+ return true;
+ }
+
+ return false;
+}
+
+export async function isIncomingIdChangeLatest(localGoalId: string, incomingChangeTimestamp: number): Promise {
+ const localGoal = await getGoalById(localGoalId);
+
+ if (!localGoal) {
+ return true;
+ }
+
+ return incomingChangeTimestamp > localGoal.timestamp;
+}
diff --git a/src/hooks/useApp.tsx b/src/hooks/useApp.tsx
index caccdbc8a..0e437b4f8 100644
--- a/src/hooks/useApp.tsx
+++ b/src/hooks/useApp.tsx
@@ -3,18 +3,20 @@ import { useEffect } from "react";
import { lastAction, displayConfirmation, openDevMode, languageSelectionState, displayToast } from "@src/store";
import { getTheme } from "@src/store/ThemeState";
-import { GoalItem } from "@src/models/GoalItem";
+import { GoalItem, IParticipant } from "@src/models/GoalItem";
import { checkMagicGoal, getAllLevelGoalsOfId, getGoal, updateSharedStatusOfGoal } from "@src/api/GoalsAPI";
import { addSharedWMGoal } from "@src/api/SharedWMAPI";
import { createDefaultGoals } from "@src/helpers/NewUserController";
import { refreshTaskCollection } from "@src/api/TasksAPI";
-import { handleIncomingChanges } from "@src/helpers/InboxProcessor";
+import { handleIncomingChanges, Payload } from "@src/helpers/InboxProcessor";
import { getContactSharedGoals, shareGoalWithContact } from "@src/services/contact.service";
import { updateAllUnacceptedContacts, getContactByRelId, clearTheQueue } from "@src/api/ContactsAPI";
import { useSetRecoilState, useRecoilValue, useRecoilState } from "recoil";
import { scheduledHintCalls } from "@src/api/HintsAPI/ScheduledHintCall";
import { LocalStorageKeys } from "@src/constants/localStorageKeys";
import { checkAndCleanupTrash } from "@src/api/TrashAPI";
+import ContactItem from "@src/models/ContactItem";
+import { SharedGoalMessage } from "@src/Interfaces/IContactMessages";
const langFromStorage = localStorage.getItem(LocalStorageKeys.LANGUAGE)?.slice(1, -1);
const exceptionRoutes = ["/", "/invest", "/feedback", "/donate"];
@@ -29,6 +31,31 @@ function useApp() {
const confirmationState = useRecoilValue(displayConfirmation);
+ const handleNewIncomingGoal = async (ele: SharedGoalMessage, contactItem: ContactItem, relId: string) => {
+ const { goalWithChildrens }: { goalWithChildrens: GoalItem[] } = ele;
+ const participant: IParticipant = {
+ name: contactItem.name,
+ relId,
+ type: "sharer",
+ following: true,
+ };
+ try {
+ await Promise.all(
+ goalWithChildrens.map(async (goal) => {
+ const goalWithParticipant = {
+ ...goal,
+ participants: [participant],
+ };
+ await addSharedWMGoal(goalWithParticipant).then(() => {
+ setLastAction("goalNewUpdates");
+ });
+ }),
+ );
+ } catch (error) {
+ console.error("[useApp] Error adding shared goals:", error);
+ }
+ };
+
useEffect(() => {
const init = async () => {
updateAllUnacceptedContacts().then(async (contacts) => {
@@ -58,7 +85,14 @@ function useApp() {
rootGoalId: goalId,
})),
]).then(async () => {
- updateSharedStatusOfGoal(goalId, relId, name).then(() => console.log("status updated"));
+ await Promise.all(
+ goalWithChildrens.map(async (goalItem) => {
+ console.log(goalItem.id, relId, name);
+ await updateSharedStatusOfGoal(goalItem.id, relId, name);
+ }),
+ ).catch((error) => {
+ console.error("[shareGoalWithRelId] Error updating shared status:", error);
+ });
});
}),
clearTheQueue(relId),
@@ -67,53 +101,24 @@ function useApp() {
);
});
const res = await getContactSharedGoals();
- // @ts-ignore
const resObject = res.response.reduce(
- (acc, curr) => ({ ...acc, [curr.relId]: [...(acc[curr.relId] || []), curr] }),
+ (acc: { [key: string]: SharedGoalMessage[] }, curr) => ({
+ ...acc,
+ [curr.relId]: [...(acc[curr.relId] || []), curr],
+ }),
{},
);
if (res.success) {
Object.keys(resObject).forEach(async (relId: string) => {
const contactItem = await getContactByRelId(relId);
if (contactItem) {
- // @ts-ignore
resObject[relId].forEach(async (ele) => {
console.log("🚀 ~ file: useApp.tsx:45 ~ resObject[relId].forEach ~ ele:", ele);
if (ele.type === "shareMessage") {
- const { goalWithChildrens }: { goalWithChildrens: GoalItem[] } = ele;
- const rootGoal = goalWithChildrens[0];
- rootGoal.participants.push({
- name: contactItem.name,
- relId,
- type: "sharer",
- following: true,
- });
- addSharedWMGoal(rootGoal)
- .then(() => {
- goalWithChildrens.slice(1).forEach((goal) => {
- addSharedWMGoal(goal).catch((err) => console.log(`Failed to add in inbox ${goal.title}`, err));
- });
- })
- .then(() => {
- setLastAction("goalNewUpdates");
- })
- .catch((err) => console.log(`Failed to add root goal ${rootGoal.title}`, err));
+ handleNewIncomingGoal(ele, contactItem, relId);
} else if (["sharer", "suggestion"].includes(ele.type)) {
- handleIncomingChanges(ele, relId).then(() => setLastAction("goalNewUpdates"));
+ handleIncomingChanges(ele as unknown as Payload, relId).then(() => setLastAction("goalNewUpdates"));
}
- // else if (["suggestion", "shared", "collaboration", "collaborationInvite"].includes(ele.type)) {
- // let typeOfSub = ele.rootGoalId ? await findTypeOfSub(ele.rootGoalId) : "none";
- // if (ele.type === "collaborationInvite") {
- // typeOfSub = "collaborationInvite";
- // } else if (["collaboration", "suggestion"].includes(ele.type)) {
- // typeOfSub = ele.type;
- // } else if (ele.type === "shared") {
- // typeOfSub = typeOfSub === "collaboration" ? "collaboration" : "shared";
- // }
- // if (typeOfSub !== "none") {
- // handleIncomingChanges({ ...ele, type: typeOfSub }).then(() => setLastAction("goalNewUpdates"));
- // }
- // }
});
}
});
diff --git a/src/hooks/useGoalActions.tsx b/src/hooks/useGoalActions.tsx
index 20d0925e7..fe42956a2 100644
--- a/src/hooks/useGoalActions.tsx
+++ b/src/hooks/useGoalActions.tsx
@@ -121,6 +121,7 @@ const useGoalActions = () => {
const shareGoalWithRelId = async (relId: string, name: string, goal: GoalItem) => {
const goalWithChildrens = await getAllLevelGoalsOfId(goal.id, true);
+
await shareGoalWithContact(relId, [
...goalWithChildrens.map((ele) => ({
...ele,
@@ -129,8 +130,16 @@ const useGoalActions = () => {
rootGoalId: goal.id,
})),
]);
- updateSharedStatusOfGoal(goal.id, relId, name).then(() => console.log("status updated"));
- showMessage(`Cheers!!, Your goal is shared with ${name}`);
+
+ await Promise.all(
+ goalWithChildrens.map(async (goalItem) => {
+ await updateSharedStatusOfGoal(goalItem.id, relId, name);
+ }),
+ ).catch((error) => {
+ console.error("[shareGoalWithRelId] Error updating shared status:", error);
+ });
+
+ showMessage(`Cheers!! Your goal and its subgoals are shared with ${name}`);
};
const addContact = async (relId: string, goalId: string) => {
@@ -157,7 +166,6 @@ const useGoalActions = () => {
goalTitle = `${goalTitle} copied!`;
showMessage("Code copied to clipboard", goalTitle);
};
-
return {
addGoal,
deleteGoalAction,
diff --git a/src/hooks/useGoalStore.tsx b/src/hooks/useGoalStore.tsx
index 238e4a495..32cc82cf7 100644
--- a/src/hooks/useGoalStore.tsx
+++ b/src/hooks/useGoalStore.tsx
@@ -1,14 +1,16 @@
import { useLocation, useNavigate, useParams } from "react-router-dom";
-import { useRecoilValue } from "recoil";
+import { useRecoilValue, useSetRecoilState } from "recoil";
import { GoalItem } from "@src/models/GoalItem";
import { displayConfirmation } from "@src/store";
import { ILocationState } from "@src/Interfaces";
+import { moveGoalState } from "@src/store/moveGoalState";
const useGoalStore = () => {
const { partnerId } = useParams();
const navigate = useNavigate();
const location = useLocation();
const showConfirmation = useRecoilValue(displayConfirmation);
+ const setGoalToMove = useSetRecoilState(moveGoalState);
const openEditMode = (goal: GoalItem, customState?: ILocationState) => {
const prefix = `${partnerId ? `/partners/${partnerId}/` : "/"}goals`;
@@ -31,10 +33,16 @@ const useGoalStore = () => {
navigate("/goals", { state: location.state });
};
+ const handleMove = (goal: GoalItem) => {
+ setGoalToMove(goal);
+ navigate("/goals", { replace: true });
+ };
+
return {
openEditMode,
handleConfirmation,
handleDisplayChanges,
+ handleMove,
};
};
diff --git a/src/models/GoalItem.ts b/src/models/GoalItem.ts
index 9471c4b96..38af50f8b 100644
--- a/src/models/GoalItem.ts
+++ b/src/models/GoalItem.ts
@@ -27,6 +27,7 @@ export interface GoalItem {
language: string;
link: string | null;
participants: IParticipant[];
+ isShared: boolean;
rootGoalId: string;
timeBudget?: {
perDay: string;
@@ -35,4 +36,5 @@ export interface GoalItem {
typeOfGoal: "myGoal" | "shared";
category: TGoalCategory;
newUpdates: boolean;
+ timestamp: number;
}
diff --git a/src/models/InboxItem.ts b/src/models/InboxItem.ts
index d8e1358be..43ba10e93 100644
--- a/src/models/InboxItem.ts
+++ b/src/models/InboxItem.ts
@@ -1,11 +1,20 @@
import { GoalItem } from "./GoalItem";
-export type typeOfChange = "subgoals" | "modifiedGoals" | "archived" | "deleted" | "restored";
+export type IdChangeTypes = "deleted" | "moved" | "restored" | "archived";
+type GoalChangeTypes = "subgoals" | "modifiedGoals" | "newGoalMoved";
+
+export type typeOfChange = IdChangeTypes | GoalChangeTypes;
export type typeOfIntent = "suggestion" | "shared";
-export type changesInId = { level: number; id: string; intent: typeOfIntent };
+export type changesInId = { level: number; id: string; intent: typeOfIntent; timestamp: number };
export type changesInGoal = { level: number; goal: GoalItem; intent: typeOfIntent };
+export type ChangesByType = {
+ [K in IdChangeTypes]: changesInId[];
+} & {
+ [K in GoalChangeTypes]: changesInGoal[];
+};
+
export interface IChangesInGoal {
subgoals: changesInGoal[];
modifiedGoals: changesInGoal[];
@@ -17,6 +26,6 @@ export interface IChangesInGoal {
export interface InboxItem {
id: string;
changes: {
- [relId: string]: IChangesInGoal;
+ [relId: string]: ChangesByType;
};
}
diff --git a/src/models/db.ts b/src/models/db.ts
index 6b5865a6a..96bf8833e 100644
--- a/src/models/db.ts
+++ b/src/models/db.ts
@@ -49,6 +49,14 @@ export class ZinZenDB extends Dexie {
console.log("🚀 ~ file: db.ts:63 ~ ZinZenDB ~ .upgrade ~ this.verno:", currentVersion);
syncVersion(db, currentVersion);
});
+ this.goalsCollection.hook("updating", (modifications: GoalItem) => {
+ modifications.timestamp = Date.now();
+ return modifications;
+ });
+
+ this.goalsCollection.hook("creating", (primKey, obj) => {
+ obj.timestamp = Date.now();
+ });
}
}
diff --git a/src/models/dexie.ts b/src/models/dexie.ts
index 423927085..c2e84d758 100644
--- a/src/models/dexie.ts
+++ b/src/models/dexie.ts
@@ -7,9 +7,9 @@ import { TaskItem } from "./TaskItem";
export const dbStoreSchema = {
feelingsCollection: "++id, content, category, date, note",
goalsCollection:
- "id, category, title, duration, sublist, habit, on, start, due, afterTime, beforeTime, createdAt, parentGoalId, archived, participants, goalColor, language, link, rootGoalId, timeBudget, typeOfGoal",
+ "id, category, title, duration, sublist, habit, on, start, due, afterTime, beforeTime, createdAt, parentGoalId, archived, participants, goalColor, language, link, rootGoalId, isShared, timeBudget, typeOfGoal, timestamp",
sharedWMCollection:
- "id, category, title, duration, sublist, repeat, start, due, afterTime, beforeTime, createdAt, parentGoalId, participants, archived, goalColor, language, link, rootGoalId, timeBudget, typeOfGoal",
+ "id, category, title, duration, sublist, repeat, start, due, afterTime, beforeTime, createdAt, parentGoalId, participants, archived, goalColor, language, link, rootGoalId, isShared, timeBudget, typeOfGoal",
contactsCollection: "id, name, relId, accepted, goalsToBeShared, createdAt, type",
outboxCollection: null,
inboxCollection: "id, goalChanges",
diff --git a/src/pages/GoalsPage/MyGoals.tsx b/src/pages/GoalsPage/MyGoals.tsx
index 4fb738aeb..ecbc7cde0 100644
--- a/src/pages/GoalsPage/MyGoals.tsx
+++ b/src/pages/GoalsPage/MyGoals.tsx
@@ -1,10 +1,9 @@
-import React, { useState, useEffect, ChangeEvent, act } from "react";
+import React, { useState, useEffect, ChangeEvent, useRef } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import ZinZenTextLight from "@assets/images/LogoTextLight.svg";
import ZinZenTextDark from "@assets/images/LogoTextDark.svg";
-import { displayChangesModal } from "@src/store/GoalsState";
import { GoalItem, TGoalCategory } from "@src/models/GoalItem";
import { GoalSublist } from "@components/GoalsComponents/GoalSublist/GoalSublist";
import { getActiveGoals } from "@api/GoalsAPI";
@@ -30,6 +29,7 @@ import { TGoalConfigMode } from "@src/types";
import { DeletedGoalProvider } from "@src/contexts/deletedGoal-context";
import { goalCategories } from "@src/constants/goals";
import { suggestedGoalState } from "@src/store/SuggestedGoalState";
+import { moveGoalState } from "@src/store/moveGoalState";
import DeletedGoals from "./components/DeletedGoals";
import ArchivedGoals from "./components/ArchivedGoals";
@@ -58,11 +58,12 @@ export const MyGoals = () => {
const suggestedGoal = useRecoilValue(suggestedGoalState);
const displaySearch = useRecoilValue(searchActive);
const darkModeStatus = useRecoilValue(darkModeState);
-
- const showChangesModal = useRecoilValue(displayChangesModal);
+ const goalToMove = useRecoilValue(moveGoalState);
const [action, setLastAction] = useRecoilState(lastAction);
+ const goalWrapperRef = useRef(null);
+
const getAllGoals = async () => {
const [goals, delGoals] = await Promise.all([getActiveGoals("true"), getDeletedGoals("root")]);
return { goals, delGoals };
@@ -99,21 +100,21 @@ export const MyGoals = () => {
useEffect(() => {
if (action === "goalArchived") return;
- if (action !== "none") {
+ if (action !== "none" || goalToMove === null) {
setLastAction("none");
refreshActiveGoals();
}
- }, [action]);
+ }, [action, goalToMove]);
useEffect(() => {
if (parentId === "root") {
refreshActiveGoals();
}
- }, [parentId, displaySearch, suggestedGoal]);
+ }, [parentId, displaySearch, suggestedGoal, goalToMove]);
return (
-
-
+
+
{showOptions && }
{showShareModal && activeGoal && }
{showParticipants && }
@@ -127,7 +128,7 @@ export const MyGoals = () => {
/>
)}
-
+
{parentId === "root" ? (
@@ -148,7 +149,7 @@ export const MyGoals = () => {
alt="Zinzen"
/>
-
-
+
+
);
};
diff --git a/src/pages/GoalsPage/PartnerGoals.tsx b/src/pages/GoalsPage/PartnerGoals.tsx
index e25757b46..9e4a0df37 100644
--- a/src/pages/GoalsPage/PartnerGoals.tsx
+++ b/src/pages/GoalsPage/PartnerGoals.tsx
@@ -56,6 +56,7 @@ const PartnerGoals = () => {
const refreshActiveGoals = async () => {
const rootGoals = await getRootGoalsOfPartner(relId);
+ console.log("rootGoals", rootGoals);
handleUserGoals(rootGoals);
};
const search = async (text: string) => {
diff --git a/src/pages/GoalsPage/components/ArchivedGoals.tsx b/src/pages/GoalsPage/components/ArchivedGoals.tsx
index 610080cba..386834f8d 100644
--- a/src/pages/GoalsPage/components/ArchivedGoals.tsx
+++ b/src/pages/GoalsPage/components/ArchivedGoals.tsx
@@ -78,7 +78,6 @@ const ArchivedGoals = ({ goals }: { goals: GoalItem[] }) => {
const [searchParams] = useSearchParams();
const { goal: activeGoal } = useActiveGoalContext();
const showOptions = !!searchParams.get("showOptions") && activeGoal?.archived === "true";
- console.log("🚀 ~ ArchivedGoals ~ showOptions:", showOptions);
return (
<>
diff --git a/src/services/contact.service.ts b/src/services/contact.service.ts
index 93995f8a5..e88be0368 100644
--- a/src/services/contact.service.ts
+++ b/src/services/contact.service.ts
@@ -1,4 +1,5 @@
import { LocalStorageKeys } from "@src/constants/localStorageKeys";
+import { SharedGoalMessageResponse } from "@src/Interfaces/IContactMessages";
import { GoalItem, IParticipant } from "@src/models/GoalItem";
import { typeOfChange } from "@src/models/InboxItem";
import { createContactRequest, getInstallId } from "@src/utils";
@@ -43,7 +44,7 @@ export const collaborateWithContact = async (relId: string, goal: GoalItem) => {
return res;
};
-export const getContactSharedGoals = async () => {
+export const getContactSharedGoals = async (): Promise
=> {
const lastProcessedTimestamp = new Date(
Number(localStorage.getItem(LocalStorageKeys.LAST_PROCESSED_TIMESTAMP)),
).toISOString();
@@ -54,7 +55,7 @@ export const getContactSharedGoals = async () => {
...(lastProcessedTimestamp ? { lastProcessedTimestamp } : {}),
});
localStorage.setItem(LocalStorageKeys.LAST_PROCESSED_TIMESTAMP, `${Date.now()}`);
- return res;
+ return res as SharedGoalMessageResponse;
};
export const getRelationshipStatus = async (relationshipId: string) => {
@@ -71,7 +72,9 @@ export const sendUpdatesToSubscriber = async (
sub: IParticipant,
rootGoalId: string,
changeType: typeOfChange,
- changes: { level: number; goal: GoalItem }[] | { level: number; id: string }[],
+ changes:
+ | { level: number; goal: Omit }[]
+ | { level: number; id: string; timestamp: number }[],
customEventType = "",
) => {
const url = "https://x7phxjeuwd4aqpgbde6f74s4ey0yobfi.lambda-url.eu-west-1.on.aws/";
diff --git a/src/short.scss b/src/short.scss
index fc49e8dc6..cca36cc97 100644
--- a/src/short.scss
+++ b/src/short.scss
@@ -96,6 +96,14 @@
font-size: 1.25rem;
}
+.text-sm {
+ font-size: 0.875rem;
+}
+
+.text-xs {
+ font-size: 0.75rem;
+}
+
.m-0 {
margin: 0;
}
diff --git a/src/store/index.ts b/src/store/index.ts
index 000d619c8..caa5b8859 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -17,6 +17,7 @@ const defaultConfirmationObj: TConfirmActionState = {
addHint: true,
deleteHint: true,
reportHint: true,
+ move: true,
},
collaboration: {
colabRequest: true,
diff --git a/src/store/moveGoalState.ts b/src/store/moveGoalState.ts
new file mode 100644
index 000000000..772022d52
--- /dev/null
+++ b/src/store/moveGoalState.ts
@@ -0,0 +1,7 @@
+import { GoalItem } from "@src/models/GoalItem";
+import { atom } from "recoil";
+
+export const moveGoalState = atom({
+ key: "moveGoalState",
+ default: null as GoalItem | null,
+});
diff --git a/src/store/useNavigateToSubgoal.ts b/src/store/useNavigateToSubgoal.ts
new file mode 100644
index 000000000..c0fb3e98f
--- /dev/null
+++ b/src/store/useNavigateToSubgoal.ts
@@ -0,0 +1,36 @@
+import { useNavigate, useLocation, useParams } from "react-router-dom";
+import { ILocationState } from "@src/Interfaces";
+import { GoalItem } from "@src/models/GoalItem";
+
+const useNavigateToSubgoal = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const { partnerId } = useParams();
+ const isPartnerModeActive = !!partnerId;
+
+ const navigateToSubgoal = (goal: GoalItem | null) => {
+ if (!goal) {
+ return;
+ }
+ const newState: ILocationState = {
+ ...location.state,
+ activeGoalId: goal.id,
+ goalsHistory: [
+ ...(location.state?.goalsHistory || []),
+ {
+ goalID: goal.id || "root",
+ goalColor: goal.goalColor || "#ffffff",
+ goalTitle: goal.title || "",
+ },
+ ],
+ };
+ const prefix = `${isPartnerModeActive ? `/partners/${partnerId}/` : "/"}goals`;
+
+ navigate(`${prefix}/${goal.id}`, { state: newState, replace: true });
+ };
+
+ return navigateToSubgoal;
+};
+
+export default useNavigateToSubgoal;
diff --git a/src/translations/en/translation.json b/src/translations/en/translation.json
index fd5cdb7d8..bd1cbe441 100644
--- a/src/translations/en/translation.json
+++ b/src/translations/en/translation.json
@@ -118,6 +118,7 @@
"addHint": "Add hint",
"deleteHint": "Delete hint",
"reportHint": "Report hint",
+ "moveGoal": "Move goal",
"confirm": "Confirm",
"cancel": "Cancel",
"duration": "Duration",
@@ -157,6 +158,10 @@
"reportHint": {
"header": "Do you want to report this hint?",
"note": "You will be notified when it's reported."
+ },
+ "move": {
+ "header": "Do you want to move this goal?",
+ "note": "- Navigate to the new location.
- Press the + button to move.
"
}
},
"collaboration": {
diff --git a/src/utils/defaultGenerators.ts b/src/utils/defaultGenerators.ts
index 4d71305e7..ca1bfe621 100644
--- a/src/utils/defaultGenerators.ts
+++ b/src/utils/defaultGenerators.ts
@@ -1,32 +1,13 @@
-import { v5 as uuidv5 } from "uuid";
+import { ChangesByType, changesInGoal, changesInId } from "@src/models/InboxItem";
-import { GoalItem } from "@src/models/GoalItem";
-import { myNameSpace } from ".";
-
-export const createPollObject = (goal: GoalItem, params: object = {}) => ({
- id: uuidv5(goal.title, myNameSpace),
- goal,
- metrics: {
- upVotes: 0,
- downVotes: 0,
- inMyGoals: 0,
- completed: 0,
- },
- myMetrics: {
- voteScore: 0,
- inMyGoals: false,
- completed: false,
- },
- createdAt: new Date().toISOString(),
- ...params,
-});
-
-export function getDefaultValueOfGoalChanges() {
+export function getDefaultValueOfGoalChanges(): ChangesByType {
return {
- subgoals: [],
- modifiedGoals: [],
- archived: [],
- deleted: [],
- restored: [],
+ subgoals: [] as changesInGoal[],
+ modifiedGoals: [] as changesInGoal[],
+ archived: [] as changesInId[],
+ deleted: [] as changesInId[],
+ restored: [] as changesInId[],
+ moved: [] as changesInId[],
+ newGoalMoved: [] as changesInGoal[],
};
}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index fe4c547e7..14769e8ea 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -43,8 +43,6 @@ export async function createGroupRequest(url: string, body: object | null = null
}
}
-export const myNameSpace = "c95256dc-aa03-11ed-afa1-0242ac120002";
-
export const getJustDate = (fullDate: Date) => new Date(fullDate.toDateString());
export const truncateContent = (content: string, maxLength = 20) => {