mirror of
https://github.com/suitenumerique/messages.git
synced 2026-09-28 04:34:56 +02:00
✨(frontend) update sender of draft message
Previously it wasn't possible to change the sender of a draft message. Now we allow that and in this case we remove the current draft and create a new one in the new mailbox. After that we redirect the user to the view of the new draft to continue it's edit!
This commit is contained in:
committed by
Jean-Baptiste PENRATH
parent
37220e615e
commit
0d214634ab
@@ -6,7 +6,7 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Message, sendCreateResponse200, useDraftCreate, useDraftUpdate2, useMessagesDestroy, useSendCreate } from "@/features/api/gen";
|
||||
import { DraftMessageRequestRequest, Message, sendCreateResponse200, useDraftCreate, useDraftUpdate2, useMessagesDestroy, useSendCreate } from "@/features/api/gen";
|
||||
import MessageEditor from "@/features/forms/components/message-editor";
|
||||
import { useMailboxContext } from "@/features/providers/mailbox";
|
||||
import MailHelper from "@/features/utils/mail-helper";
|
||||
@@ -14,6 +14,7 @@ import { RhfInput, RhfSelect } from "../react-hook-form";
|
||||
import { addToast, ToasterItem } from "@/features/ui/components/toaster";
|
||||
import { toast } from "react-toastify";
|
||||
import { useSentBox } from "@/features/providers/sent-box";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface MessageFormProps {
|
||||
// For reply mode
|
||||
@@ -67,13 +68,18 @@ export const MessageForm = ({
|
||||
onSuccess
|
||||
}: MessageFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [draft, setDraft] = useState<Message | undefined>(draftMessage);
|
||||
const [showCCField, setShowCCField] = useState((draftMessage?.cc?.length ?? 0) > 0);
|
||||
const [showBCCField, setShowBCCField] = useState((draftMessage?.bcc?.length ?? 0) > 0);
|
||||
const [pendingSubmit, setPendingSubmit] = useState(false);
|
||||
const { selectedMailbox, mailboxes, invalidateThreadMessages, invalidateThreadsStats, unselectThread } = useMailboxContext();
|
||||
const hideSubjectField = Boolean(parentMessage);
|
||||
const hideFromField = (mailboxes?.length ?? 0) === 0 || draft;
|
||||
const defaultSenderId = mailboxes?.find((mailbox) => {
|
||||
if (draft?.sender) return draft.sender.email === mailbox.email;
|
||||
return selectedMailbox?.id === mailbox.id;
|
||||
})?.id ?? mailboxes?.[0]?.id;
|
||||
const hideFromField = defaultSenderId && (mailboxes?.length ?? 0) === 1;
|
||||
const { addQueuedMessage } = useSentBox();
|
||||
|
||||
const getMailboxOptions = () => {
|
||||
@@ -114,7 +120,7 @@ export const MessageForm = ({
|
||||
}, [parentMessage, replyAll, selectedMailbox]);
|
||||
|
||||
const formDefaultValues = useMemo(() => ({
|
||||
from: selectedMailbox?.id || mailboxes?.[0]?.id || '',
|
||||
from: defaultSenderId ?? '',
|
||||
to: (draft?.to?.map(contact => contact.email) ?? recipients).join(', '),
|
||||
cc: (draft?.cc?.map(contact => contact.email) ?? []).join(', '),
|
||||
bcc: (draft?.bcc?.map(contact => contact.email) ?? []).join(', '),
|
||||
@@ -171,32 +177,58 @@ export const MessageForm = ({
|
||||
mutation: { onSuccess: handleDraftMutationSuccess }
|
||||
});
|
||||
|
||||
const deleteMessageMutation = useMessagesDestroy({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setDraft(undefined);
|
||||
invalidateThreadMessages();
|
||||
invalidateThreadsStats();
|
||||
unselectThread();
|
||||
addToast(
|
||||
<ToasterItem type="info">
|
||||
|
||||
<span>{t("message_form.success.draft_deleted")}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
onClose?.();
|
||||
},
|
||||
}
|
||||
});
|
||||
const deleteMessageMutation = useMessagesDestroy();
|
||||
|
||||
const handleDeleteMessage = (messageId: string) => {
|
||||
if(window.confirm(t("message_form.confirm.delete"))) {
|
||||
deleteMessageMutation.mutate({
|
||||
id: messageId
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setDraft(undefined);
|
||||
invalidateThreadMessages();
|
||||
invalidateThreadsStats();
|
||||
unselectThread();
|
||||
addToast(
|
||||
<ToasterItem type="info">
|
||||
|
||||
<span>{t("message_form.success.draft_deleted")}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user changes the message sender, we need to delete the draft,
|
||||
* then recreate a new one. Once the new draft is created, we need to
|
||||
* redirect the user to the new draft view.
|
||||
*/
|
||||
const handleChangeSender = async (data: DraftMessageRequestRequest) => {
|
||||
if (draft && form.formState.dirtyFields.from) {
|
||||
await deleteMessageMutation.mutateAsync({ id: draft.id });
|
||||
const response = await draftCreateMutation.mutateAsync({ data }, {
|
||||
onSuccess: () => {addToast(
|
||||
<ToasterItem type="info">
|
||||
<span>{t("message_form.success.draft_transferred")}</span>
|
||||
</ToasterItem>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if(router.asPath.includes("new")) {
|
||||
setDraft(response.data as Message);
|
||||
return;
|
||||
}
|
||||
const mailboxId = data.senderId;
|
||||
const threadId = response.data.thread_id
|
||||
// @TODO: Make something less hardcoded to improve the maintainability of the code
|
||||
router.replace(`/mailbox/${mailboxId}/thread/${threadId}?has_draft=1`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create a draft message if any field to change.
|
||||
*/
|
||||
@@ -212,15 +244,19 @@ export const MessageForm = ({
|
||||
cc: data.cc || [],
|
||||
bcc: data.bcc || [],
|
||||
subject: subject,
|
||||
senderId: data.from!,
|
||||
senderId: data.from,
|
||||
parentId: parentMessage?.id,
|
||||
draftBody: data.messageEditorDraft,
|
||||
}
|
||||
let response;
|
||||
|
||||
if (!draft) {
|
||||
response = await draftCreateMutation.mutateAsync({
|
||||
data: payload,
|
||||
});
|
||||
} else if (form.formState.dirtyFields.from) {
|
||||
handleChangeSender(payload);
|
||||
return;
|
||||
} else {
|
||||
response = await draftUpdateMutation.mutateAsync({
|
||||
messageId: draft.id,
|
||||
@@ -269,7 +305,7 @@ export const MessageForm = ({
|
||||
if (draftMessage) form.setFocus("subject");
|
||||
else form.setFocus("to")
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (draft) {
|
||||
form.reset(undefined, { keepSubmitCount: true, keepDirty: false, keepValues: true, keepDefaultValues: false });
|
||||
|
||||
@@ -17,10 +17,10 @@ export const RhfSelect = (props: SelectProps & { name: string }) => {
|
||||
render={({ field, fieldState }) => {
|
||||
return (
|
||||
<Select
|
||||
{...field}
|
||||
{...props}
|
||||
aria-invalid={!!fieldState.error}
|
||||
state={fieldState.error ? "error" : "default"}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(e) => setValue(field.name, e.target.value, { shouldDirty: true })}
|
||||
value={field.value}
|
||||
/>
|
||||
|
||||
@@ -68,7 +68,8 @@
|
||||
},
|
||||
"success": {
|
||||
"saved": "Draft saved",
|
||||
"draft_deleted": "Draft deleted"
|
||||
"draft_deleted": "Draft deleted",
|
||||
"draft_transferred": "Draft transferred to another mailbox"
|
||||
},
|
||||
"confirm": {
|
||||
"delete": "Are you sure you want to delete this draft? This action cannot be undone."
|
||||
@@ -178,7 +179,8 @@
|
||||
},
|
||||
"success": {
|
||||
"saved": "Brouillon sauvegardé.",
|
||||
"draft_deleted": "Brouillon supprimé."
|
||||
"draft_deleted": "Brouillon supprimé.",
|
||||
"draft_transferred": "Brouillon transféré vers une autre boîte aux lettres."
|
||||
},
|
||||
"confirm": {
|
||||
"delete": "Êtes-vous sûr de vouloir supprimer ce brouillon ? Cette action est irréversible."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { useParams, useSearchParams } from "next/navigation"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { ActionBar } from "./components/thread-action-bar"
|
||||
import { ThreadMessage } from "./components/thread-message"
|
||||
import { useMailboxContext } from "@/features/providers/mailbox"
|
||||
@@ -13,7 +13,6 @@ type MessageWithDraftChild = Message & {
|
||||
}
|
||||
|
||||
export const ThreadView = () => {
|
||||
const params = useParams<{ mailboxId: string, threadId: string }>()
|
||||
const searchParams = useSearchParams();
|
||||
const toMarkAsReadQueue = useRef<string[]>([]);
|
||||
const debouncedMarkAsRead = useDebounceCallback(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from "react";
|
||||
import { Mailbox, PaginatedMessageList, PaginatedThreadList, Thread, useMailboxesList, useMessagesList, useThreadsListInfinite } from "../api/gen";
|
||||
import { FetchStatus, QueryStatus, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -2,10 +2,13 @@ import { MainLayout } from "@/features/layouts/components/main";
|
||||
import { MessageForm } from "@/features/forms/components/message-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMailboxContext } from "@/features/providers/mailbox";
|
||||
import { Spinner } from "@gouvfr-lasuite/ui-kit";
|
||||
|
||||
const NewMessageFormPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { queryStates } = useMailboxContext();
|
||||
|
||||
/**
|
||||
* Go back to the previous page or to
|
||||
@@ -19,6 +22,14 @@ const NewMessageFormPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (queryStates.mailboxes.isLoading) {
|
||||
return (
|
||||
<div className="thread-view thread-view--loading">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="new-message-form-container">
|
||||
<h1>{t("new_message_form.title")}</h1>
|
||||
|
||||
Reference in New Issue
Block a user