mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-15 13:01:47 +00:00
feat(notes): edição inline de tarefas no modal de anotações
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RiAddCircleFill, RiDeleteBinLine } from "@remixicon/react";
|
import {
|
||||||
|
RiAddCircleFill,
|
||||||
|
RiCheckLine,
|
||||||
|
RiDeleteBinLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -69,10 +73,13 @@ export function NoteDialog({
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [newTaskText, setNewTaskText] = useState("");
|
const [newTaskText, setNewTaskText] = useState("");
|
||||||
|
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
|
||||||
|
const [editingTaskText, setEditingTaskText] = useState("");
|
||||||
|
|
||||||
const titleRef = useRef<HTMLInputElement>(null);
|
const titleRef = useRef<HTMLInputElement>(null);
|
||||||
const descRef = useRef<HTMLTextAreaElement>(null);
|
const descRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const newTaskRef = useRef<HTMLInputElement>(null);
|
const newTaskRef = useRef<HTMLInputElement>(null);
|
||||||
|
const editingTaskRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||||
open,
|
open,
|
||||||
@@ -90,6 +97,8 @@ export function NoteDialog({
|
|||||||
resetForm(buildInitialValues(note));
|
resetForm(buildInitialValues(note));
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setNewTaskText("");
|
setNewTaskText("");
|
||||||
|
setEditingTaskId(null);
|
||||||
|
setEditingTaskText("");
|
||||||
requestAnimationFrame(() => titleRef.current?.focus());
|
requestAnimationFrame(() => titleRef.current?.focus());
|
||||||
}
|
}
|
||||||
}, [dialogOpen, note, resetForm]);
|
}, [dialogOpen, note, resetForm]);
|
||||||
@@ -126,7 +135,12 @@ export function NoteDialog({
|
|||||||
formState.description.trim() === (note?.description ?? "").trim() &&
|
formState.description.trim() === (note?.description ?? "").trim() &&
|
||||||
JSON.stringify(formState.tasks) === JSON.stringify(note?.tasks);
|
JSON.stringify(formState.tasks) === JSON.stringify(note?.tasks);
|
||||||
|
|
||||||
const disableSubmit = isPending || onlySpaces || unchanged || invalidLen;
|
const disableSubmit =
|
||||||
|
isPending ||
|
||||||
|
onlySpaces ||
|
||||||
|
unchanged ||
|
||||||
|
invalidLen ||
|
||||||
|
Boolean(editingTaskId);
|
||||||
|
|
||||||
const handleOpenChange = (v: boolean) => {
|
const handleOpenChange = (v: boolean) => {
|
||||||
setDialogOpen(v);
|
setDialogOpen(v);
|
||||||
@@ -159,6 +173,10 @@ export function NoteDialog({
|
|||||||
"tasks",
|
"tasks",
|
||||||
(formState.tasks || []).filter((t) => t.id !== taskId),
|
(formState.tasks || []).filter((t) => t.id !== taskId),
|
||||||
);
|
);
|
||||||
|
if (editingTaskId === taskId) {
|
||||||
|
setEditingTaskId(null);
|
||||||
|
setEditingTaskText("");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleTask = (taskId: string) => {
|
const handleToggleTask = (taskId: string) => {
|
||||||
@@ -170,6 +188,40 @@ export function NoteDialog({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStartEditTask = (task: Task) => {
|
||||||
|
if (isPending) return;
|
||||||
|
|
||||||
|
setEditingTaskId(task.id);
|
||||||
|
setEditingTaskText(task.text);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
editingTaskRef.current?.focus();
|
||||||
|
editingTaskRef.current?.select();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveTask = (taskId: string) => {
|
||||||
|
const text = normalize(editingTaskText);
|
||||||
|
if (!text) {
|
||||||
|
toast.error("O texto da tarefa não pode estar vazio.");
|
||||||
|
editingTaskRef.current?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateField(
|
||||||
|
"tasks",
|
||||||
|
(formState.tasks || []).map((t) =>
|
||||||
|
t.id === taskId ? { ...t, text } : t,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setEditingTaskId(null);
|
||||||
|
setEditingTaskText("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEditTask = () => {
|
||||||
|
setEditingTaskId(null);
|
||||||
|
setEditingTaskText("");
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
@@ -373,33 +425,78 @@ export function NoteDialog({
|
|||||||
key={task.id}
|
key={task.id}
|
||||||
className="flex items-center gap-3 rounded-md px-3 py-1.5 hover:bg-muted/50"
|
className="flex items-center gap-3 rounded-md px-3 py-1.5 hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<Checkbox
|
{editingTaskId === task.id ? (
|
||||||
className="data-[state=checked]:bg-success! data-[state=checked]:border-success! data-[state=checked]:text-success-foreground!"
|
<Input
|
||||||
checked={task.completed}
|
ref={editingTaskRef}
|
||||||
onCheckedChange={() => handleToggleTask(task.id)}
|
value={editingTaskText}
|
||||||
disabled={isPending}
|
onChange={(e) => setEditingTaskText(e.target.value)}
|
||||||
aria-label={`Marcar "${task.text}" como ${
|
onKeyDown={(e) => {
|
||||||
task.completed ? "não concluída" : "concluída"
|
if (e.key === "Enter") {
|
||||||
}`}
|
e.preventDefault();
|
||||||
/>
|
e.stopPropagation();
|
||||||
<span
|
handleSaveTask(task.id);
|
||||||
className={cn(
|
}
|
||||||
"flex-1 text-sm wrap-break-word",
|
if (e.key === "Escape") {
|
||||||
task.completed
|
e.preventDefault();
|
||||||
? "text-muted-foreground line-through"
|
e.stopPropagation();
|
||||||
: "text-foreground",
|
handleCancelEditTask();
|
||||||
)}
|
}
|
||||||
>
|
}}
|
||||||
{task.text}
|
disabled={isPending}
|
||||||
</span>
|
className="h-8 min-w-0 flex-1"
|
||||||
|
aria-label={`Editar "${task.text}"`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Checkbox
|
||||||
|
className="data-[state=checked]:bg-success! data-[state=checked]:border-success! data-[state=checked]:text-success-foreground!"
|
||||||
|
checked={task.completed}
|
||||||
|
onCheckedChange={() => handleToggleTask(task.id)}
|
||||||
|
disabled={isPending}
|
||||||
|
aria-label={`Marcar "${task.text}" como ${
|
||||||
|
task.completed ? "não concluída" : "concluída"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleStartEditTask(task)}
|
||||||
|
disabled={isPending}
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 flex-1 cursor-text text-left text-sm wrap-break-word transition-colors hover:text-primary disabled:cursor-not-allowed",
|
||||||
|
task.completed
|
||||||
|
? "text-muted-foreground line-through"
|
||||||
|
: "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{task.text}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemoveTask(task.id)}
|
onClick={() =>
|
||||||
|
editingTaskId === task.id
|
||||||
|
? handleSaveTask(task.id)
|
||||||
|
: handleRemoveTask(task.id)
|
||||||
|
}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="shrink-0 text-muted-foreground/50 hover:text-destructive transition-colors"
|
className={cn(
|
||||||
aria-label={`Remover "${task.text}"`}
|
"shrink-0 transition-colors",
|
||||||
|
editingTaskId === task.id
|
||||||
|
? "text-success hover:text-success/80"
|
||||||
|
: "text-muted-foreground/50 hover:text-destructive",
|
||||||
|
)}
|
||||||
|
aria-label={
|
||||||
|
editingTaskId === task.id
|
||||||
|
? `Salvar "${task.text}"`
|
||||||
|
: `Remover "${task.text}"`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className="h-3.5 w-3.5" />
|
{editingTaskId === task.id ? (
|
||||||
|
<RiCheckLine className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<RiDeleteBinLine className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user