Skip to content

Commit 576dfcc

Browse files
khardikkMrgSub
andauthored
Feat: label creation flow fix (#1908)
Co-authored-by: Adam <[email protected]>
1 parent 70051c0 commit 576dfcc

File tree

3 files changed

+86
-15
lines changed

3 files changed

+86
-15
lines changed

apps/mail/components/context/thread-context.tsx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ import {
2121
Star,
2222
StarOff,
2323
Tag,
24+
Plus,
2425
Trash,
2526
} from 'lucide-react';
2627
import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-state';
28+
import { LabelDialog } from '@/components/labels/label-dialog';
2729
import { useOptimisticActions } from '@/hooks/use-optimistic-actions';
2830
import { ExclamationCircle, Mail, Clock } from '../icons/icons';
2931
import { SnoozeDialog } from '@/components/mail/snooze-dialog';
3032
import { type ThreadDestination } from '@/lib/thread-actions';
3133
import { useThread, useThreads } from '@/hooks/use-threads';
32-
import { useMemo, type ReactNode, useState } from 'react';
34+
import { useMemo, type ReactNode, useState, useCallback } from 'react';
3335
import { useTRPC } from '@/providers/query-provider';
3436
import { useMutation } from '@tanstack/react-query';
3537
import { useLabels } from '@/hooks/use-labels';
@@ -40,6 +42,7 @@ import { m } from '@/paraglide/messages';
4042
import { useParams } from 'react-router';
4143
import { useQueryState } from 'nuqs';
4244
import { toast } from 'sonner';
45+
import type { Label as LabelType } from '@/types';
4346

4447
interface EmailAction {
4548
id: string;
@@ -61,7 +64,7 @@ interface EmailContextMenuProps {
6164
refreshCallback?: () => void;
6265
}
6366

64-
const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected: string[] }) => {
67+
const LabelsList = ({ threadId, bulkSelected, onCreateLabel }: { threadId: string; bulkSelected: string[]; onCreateLabel: () => void }) => {
6568
const { userLabels: labels } = useLabels();
6669
const { optimisticToggleLabel } = useOptimisticActions();
6770
const targetThreadIds = bulkSelected.length > 0 ? bulkSelected : [threadId];
@@ -90,6 +93,19 @@ const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected
9093
optimisticToggleLabel(targetThreadIds, labelId, !hasLabel);
9194
};
9295

96+
// If no labels exist, show create label button
97+
if (!labels || labels.length === 0) {
98+
return (
99+
<ContextMenuItem
100+
onClick={onCreateLabel}
101+
className="font-normal"
102+
>
103+
<Plus className="mr-2 h-4 w-4 opacity-60" />
104+
{m['common.mail.createNewLabel']()}
105+
</ContextMenuItem>
106+
);
107+
}
108+
93109
return (
94110
<>
95111
{labels
@@ -148,6 +164,7 @@ export function ThreadContextMenu({
148164
const [, setActiveReplyId] = useQueryState('activeReplyId');
149165
const optimisticState = useOptimisticThreadState(threadId);
150166
const trpc = useTRPC();
167+
const { refetch: refetchLabels } = useLabels();
151168
const {
152169
optimisticMoveThreadsTo,
153170
optimisticToggleStar,
@@ -159,6 +176,7 @@ export function ThreadContextMenu({
159176
optimisticUnsnooze,
160177
} = useOptimisticActions();
161178
const { mutateAsync: deleteThread } = useMutation(trpc.mail.delete.mutationOptions());
179+
const { mutateAsync: createLabel } = useMutation(trpc.labels.create.mutationOptions());
162180

163181
const { isUnread, isStarred, isImportant } = useMemo(() => {
164182
const unread = threadData?.hasUnread ?? false;
@@ -454,13 +472,47 @@ export function ThreadContextMenu({
454472
}, [isSpam, isBin, isArchiveFolder, isInbox, isSent, handleMove, handleDelete]);
455473

456474
const [snoozeOpen, setSnoozeOpen] = useState(false);
475+
const [createLabelOpen, setCreateLabelOpen] = useState(false);
476+
477+
const handleOpenCreateLabel = useCallback(() => {
478+
setCreateLabelOpen(true);
479+
}, []);
457480

458481
const handleSnoozeConfirm = (wakeAt: Date) => {
459482
const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];
460483
optimisticSnooze(targets, currentFolder, wakeAt);
461484
setSnoozeOpen(false);
462485
};
463486

487+
const handleCreateLabel = async (data: LabelType) => {
488+
const labelData = {
489+
name: data.name,
490+
color: {
491+
backgroundColor: data.color?.backgroundColor || '#202020',
492+
textColor: data.color?.textColor || '#FFFFFF'
493+
}
494+
};
495+
496+
try {
497+
const promise = createLabel(labelData).then(async (result) => {
498+
await refetchLabels();
499+
return result;
500+
});
501+
502+
toast.promise(promise, {
503+
loading: m['common.labels.savingLabel'](),
504+
success: m['common.labels.saveLabelSuccess'](),
505+
error: m['common.labels.failedToSavingLabel'](),
506+
});
507+
508+
await promise;
509+
} catch (error) {
510+
console.error('Failed to create label:', error);
511+
} finally {
512+
setCreateLabelOpen(false);
513+
}
514+
};
515+
464516
const otherActions: EmailAction[] = useMemo(
465517
() => [
466518
{
@@ -520,6 +572,11 @@ export function ThreadContextMenu({
520572

521573
return (
522574
<>
575+
<LabelDialog
576+
open={createLabelOpen}
577+
onOpenChange={setCreateLabelOpen}
578+
onSubmit={handleCreateLabel}
579+
/>
523580
<ContextMenu>
524581
<ContextMenuTrigger disabled={isLoading || isFetching} className="w-full">
525582
{children}
@@ -538,7 +595,7 @@ export function ThreadContextMenu({
538595
{m['common.mail.labels']()}
539596
</ContextMenuSubTrigger>
540597
<ContextMenuSubContent className="dark:bg-panelDark max-h-[520px] w-48 overflow-y-auto bg-white">
541-
<LabelsList threadId={threadId} bulkSelected={mail.bulkSelected} />
598+
<LabelsList threadId={threadId} bulkSelected={mail.bulkSelected} onCreateLabel={handleOpenCreateLabel} />
542599
</ContextMenuSubContent>
543600
</ContextMenuSub>
544601

apps/mail/components/labels/label-dialog.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Dialog,
33
DialogContent,
4+
DialogDescription,
45
DialogHeader,
56
DialogTitle,
67
DialogTrigger,
@@ -64,12 +65,12 @@ export function LabelDialog({
6465
if (editingLabel) {
6566
form.reset({
6667
name: editingLabel.name,
67-
color: editingLabel.color || { backgroundColor: '#E2E2E2', textColor: '#000000' },
68+
color: editingLabel.color || { backgroundColor: '#202020', textColor: '#FFFFFF' },
6869
});
6970
} else {
7071
form.reset({
7172
name: '',
72-
color: { backgroundColor: '#E2E2E2', textColor: '#000000' },
73+
color: { backgroundColor: '#202020', textColor: '#FFFFFF' },
7374
});
7475
}
7576
}
@@ -85,7 +86,7 @@ export function LabelDialog({
8586
setDialogOpen(false);
8687
form.reset({
8788
name: '',
88-
color: { backgroundColor: '#E2E2E2', textColor: '#000000' },
89+
color: { backgroundColor: '#202020', textColor: '#FFFFFF' },
8990
});
9091
};
9192

@@ -97,6 +98,11 @@ export function LabelDialog({
9798
<DialogTitle>
9899
{editingLabel ? m['common.labels.editLabel']() : m['common.mail.createNewLabel']()}
99100
</DialogTitle>
101+
<DialogDescription>
102+
{editingLabel
103+
? 'Modify the label name and color to update this label.'
104+
: 'Create a new label to organize your emails. Choose a name and color for easy identification.'}
105+
</DialogDescription>
100106
</DialogHeader>
101107
<Form {...form}>
102108
<form
@@ -126,7 +132,7 @@ export function LabelDialog({
126132
<div className="space-y-2">
127133
<Label>{m['common.labels.color']()}</Label>
128134
<div className="w-full">
129-
<div className="flex flex-wrap gap-2">
135+
<div className="flex flex-wrap gap-2 mt-2">
130136
{LABEL_COLORS.map((color) => (
131137
<button
132138
key={color.backgroundColor}

apps/mail/components/ui/nav-main.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,22 @@ export function NavMain({ items }: NavMainProps) {
163163
);
164164

165165
const onSubmit = async (data: LabelType) => {
166-
toast.promise(createLabel(data), {
167-
loading: 'Creating label...',
168-
success: 'Label created successfully',
169-
error: 'Failed to create label',
170-
finally: () => {
171-
refetch();
172-
},
173-
});
166+
try {
167+
const promise = createLabel(data).then(async (result) => {
168+
await refetch();
169+
return result;
170+
});
171+
172+
toast.promise(promise, {
173+
loading: 'Creating label...',
174+
success: 'Label created successfully',
175+
error: 'Failed to create label',
176+
});
177+
178+
await promise;
179+
} catch (error) {
180+
console.error('Failed to create label:', error);
181+
}
174182
};
175183

176184
return (

0 commit comments

Comments
 (0)