Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions apps/console/src/_locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -779,12 +779,14 @@
"assignedTo": { "label": "Assigned to" },
"measure": { "label": "Measure" },
"timeEstimate": { "label": "Time estimate" },
"deadline": { "label": "Deadline" }
"deadline": { "label": "Deadline" },
"recurrence": { "label": "Repeats" }
},
"states": { "todo": "To do", "inProgress": "In progress", "done": "Done" },
"priorities": { "urgent": "Urgent", "high": "High", "medium": "Medium", "low": "Low" },
"recurrenceIntervalUnits": { "none": "Does not repeat", "day": "Day", "week": "Week", "month": "Month", "year": "Year" },
"messages": { "created": "Task created successfully.", "updated": "Task updated successfully." },
"errors": { "create": "Failed to create task", "update": "Failed to update task" },
"errors": { "create": "Failed to create task", "update": "Failed to update task", "recurrenceRequiresDeadline": "Deadline is required for a recurring task", "recurrenceRequiresCount": "Recurrence count is required", "recurrenceRequiresUnit": "Recurrence unit is required" },
"actions": { "create": "Create task", "update": "Update task" }
},
"tasksCard": {
Expand All @@ -806,7 +808,11 @@
"days_other": "{{count}} Days"
},
"deleteConfirmation": "Are you sure you want to delete this task?",
"actions": { "moveToInProgress": "Move to In progress", "moveToDone": "Move to Done", "edit": "Edit", "delete": "Delete" }
"actions": { "moveToInProgress": "Move to In progress", "moveToDone": "Move to Done", "edit": "Edit", "delete": "Delete" },
"recurringBadge": {
"tooltip_one": "Repeats every {{unit}}",
"tooltip_other": "Repeats every {{count}} {{unit}}s"
}
},
"deleteTrustCenterReferenceDialog": {
"title": "Delete Reference",
Expand Down
19 changes: 18 additions & 1 deletion apps/console/src/_locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,9 @@
},
"deadline": {
"label": "Échéance"
},
"recurrence": {
"label": "Répétition"
}
},
"states": {
Expand All @@ -1368,13 +1371,23 @@
"medium": "Moyenne",
"low": "Basse"
},
"recurrenceIntervalUnits": {
"none": "Ne se répète pas",
"day": "Jour",
"week": "Semaine",
"month": "Mois",
"year": "Année"
},
"messages": {
"created": "Tâche créée avec succès.",
"updated": "Tâche mise à jour avec succès."
},
"errors": {
"create": "Échec de la création de la tâche",
"update": "Échec de la mise à jour de la tâche"
"update": "Échec de la mise à jour de la tâche",
"recurrenceRequiresDeadline": "Une échéance est requise pour une tâche récurrente",
"recurrenceRequiresCount": "Le nombre de répétitions est requis",
"recurrenceRequiresUnit": "L'unité de répétition est requise"
},
"actions": {
"create": "Créer la tâche",
Expand Down Expand Up @@ -1413,6 +1426,10 @@
"moveToDone": "Déplacer vers Terminé",
"edit": "Modifier",
"delete": "Supprimer"
},
"recurringBadge": {
"tooltip_one": "Se répète tous les {{count}} {{unit}}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The recurringBadge tooltip renders improperly in French for singular counts. tooltip_one should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/_locales/fr-FR.json, line 1429:

<comment>The recurringBadge tooltip renders improperly in French for singular counts. `tooltip_one` should use a singular-aware template such as "Se répète chaque {{unit}}" instead of "Se répète tous les {{count}} {{unit}}", which is ungrammatical when count=1 ("tous les 1 jour").</comment>

<file context>
@@ -1413,6 +1424,10 @@
       "delete": "Supprimer"
+    },
+    "recurringBadge": {
+      "tooltip_one": "Se répète tous les {{count}} {{unit}}",
+      "tooltip_other": "Se répète tous les {{count}} {{unit}}"
     }
</file context>

"tooltip_other": "Se répète tous les {{count}} {{unit}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (2 Semaine, 2 Année). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/_locales/fr-FR.json, line 1430:

<comment>The recurring-task tooltip is grammatically incorrect in French for counts greater than one because both plural branches reuse the singular translated unit (`2 Semaine`, `2 Année`). Providing pluralized unit translations or unit-specific tooltip variants would make the badge readable for all recurrence units.</comment>

<file context>
@@ -1413,6 +1424,10 @@
+    },
+    "recurringBadge": {
+      "tooltip_one": "Se répète tous les {{count}} {{unit}}",
+      "tooltip_other": "Se répète tous les {{count}} {{unit}}"
     }
   },
</file context>

}
},
"deleteTrustCenterReferenceDialog": {
Expand Down
102 changes: 97 additions & 5 deletions apps/console/src/components/tasks/TaskFormDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const taskFragment = graphql`
priority
timeEstimate
deadline
recurrenceIntervalUnit
recurrenceIntervalCount
assignedTo {
id
}
Expand Down Expand Up @@ -101,8 +103,49 @@ export const taskUpdateMutation = graphql`

export const taskStates = ["TODO", "IN_PROGRESS", "DONE"] as const;
export const taskPriorities = ["URGENT", "HIGH", "MEDIUM", "LOW"] as const;
export const taskRecurrenceIntervalUnits = ["DAY", "WEEK", "MONTH", "YEAR"] as const;

const createTaskSchema = z.object({
const recurrenceIntervalUnitField = z.preprocess(
val => (val === "" || val == null ? null : val),
z.enum(taskRecurrenceIntervalUnits).nullable().optional(),
);

const recurrenceIntervalCountField = z.preprocess(
val => (typeof val === "number" && Number.isNaN(val) ? null : val),
z.number().int().min(1).nullable().optional(),
);

// Recurrence refinements emit translation keys instead of literal messages so
// the form can localize them at render time.
const recurrenceErrorKeys = [
"taskFormDialog.errors.recurrenceRequiresCount",
"taskFormDialog.errors.recurrenceRequiresUnit",
"taskFormDialog.errors.recurrenceRequiresDeadline",
] as const;

const recurrenceErrorKeySet = new Set<string>(recurrenceErrorKeys);

function refineRecurrence<T extends z.ZodType<{
deadline?: string | null;
recurrenceIntervalUnit?: string | null;
recurrenceIntervalCount?: number | null;
}>>(schema: T) {
return schema
.refine(
data => !(data.recurrenceIntervalUnit && !data.recurrenceIntervalCount),
{ message: recurrenceErrorKeys[0], path: ["recurrenceIntervalCount"] },
)
.refine(
data => !(data.recurrenceIntervalCount && !data.recurrenceIntervalUnit),
{ message: recurrenceErrorKeys[1], path: ["recurrenceIntervalUnit"] },
)
.refine(
data => !(data.recurrenceIntervalUnit && !data.deadline),
{ message: recurrenceErrorKeys[2], path: ["deadline"] },
);
}

const createTaskSchema = refineRecurrence(z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
priority: z.enum(taskPriorities),
Expand All @@ -113,9 +156,11 @@ const createTaskSchema = z.object({
z.string().nullable().optional(),
),
deadline: z.string().optional().nullable(),
});
recurrenceIntervalUnit: recurrenceIntervalUnitField,
recurrenceIntervalCount: recurrenceIntervalCountField,
}));

const updateTaskSchema = z.object({
const updateTaskSchema = refineRecurrence(z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
state: z.enum(taskStates),
Expand All @@ -130,7 +175,9 @@ const updateTaskSchema = z.object({
z.string().nullable().optional(),
),
deadline: z.string().optional().nullable(),
});
recurrenceIntervalUnit: recurrenceIntervalUnitField,
recurrenceIntervalCount: recurrenceIntervalCountField,
}));

type Props = {
children?: ReactNode;
Expand Down Expand Up @@ -170,6 +217,8 @@ export default function TaskFormDialog(props: Props) {
assignedToId: task?.assignedTo?.id ?? "",
measureId: task?.measure?.id ?? measureId ?? "",
deadline: task?.deadline?.split("T")[0] ?? "",
recurrenceIntervalUnit: task?.recurrenceIntervalUnit ?? "",
recurrenceIntervalCount: task?.recurrenceIntervalCount ?? 1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Non-recurring tasks cannot be submitted with the new defaults: the form starts with count 1 and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores 1; initializing the count as null when no unit is present (or clearing it when the unit becomes none) would preserve the intended optional recurrence behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/components/tasks/TaskFormDialog.tsx, line 211:

<comment>Non-recurring tasks cannot be submitted with the new defaults: the form starts with count `1` and no recurrence unit, while the new refinement treats that as an invalid count-without-unit combination. Clearing recurrence on an existing task has the same problem because the reset path restores `1`; initializing the count as `null` when no unit is present (or clearing it when the unit becomes `none`) would preserve the intended optional recurrence behavior.</comment>

<file context>
@@ -170,6 +207,8 @@ export default function TaskFormDialog(props: Props) {
         measureId: task?.measure?.id ?? measureId ?? "",
         deadline: task?.deadline?.split("T")[0] ?? "",
+        recurrenceIntervalUnit: task?.recurrenceIntervalUnit ?? "",
+        recurrenceIntervalCount: task?.recurrenceIntervalCount ?? 1,
       },
     });
</file context>

},
});

Expand All @@ -184,12 +233,18 @@ export default function TaskFormDialog(props: Props) {
assignedToId: task.assignedTo?.id ?? "",
measureId: task.measure?.id ?? measureId ?? "",
deadline: task.deadline?.split("T")[0] ?? "",
recurrenceIntervalUnit: task.recurrenceIntervalUnit ?? "",
recurrenceIntervalCount: task.recurrenceIntervalCount ?? 1,
});
}
}, [
task, reset, measureId,
]);

// Zod default messages come back as plain text, recurrence ones as keys.
const translateError = (message?: string) =>
message && recurrenceErrorKeySet.has(message) ? t(message) : message;

const onSubmit = async (data: z.infer<typeof updateTaskSchema | typeof createTaskSchema>) => {
if (task) {
await mutate({
Expand All @@ -204,6 +259,8 @@ export default function TaskFormDialog(props: Props) {
deadline: formatDatetime(data.deadline) ?? null,
assignedToId: data.assignedToId ?? null,
measureId: data.measureId || null,
recurrenceIntervalUnit: data.recurrenceIntervalUnit || null,
recurrenceIntervalCount: data.recurrenceIntervalUnit ? data.recurrenceIntervalCount : null,
},
},
onCompleted: (_response, errors) => {
Expand All @@ -222,6 +279,8 @@ export default function TaskFormDialog(props: Props) {
deadline: formatDatetime(data.deadline) ?? null,
assignedToId: data.assignedToId || null,
measureId: data.measureId || null,
recurrenceIntervalUnit: data.recurrenceIntervalUnit || null,
recurrenceIntervalCount: data.recurrenceIntervalUnit ? data.recurrenceIntervalCount : null,
},
connections: [connection!],
},
Expand Down Expand Up @@ -389,10 +448,43 @@ export default function TaskFormDialog(props: Props) {
</PropertyRow>
<PropertyRow
label={t("taskFormDialog.fields.deadline.label")}
error={formState.errors.deadline?.message}
error={translateError(formState.errors.deadline?.message)}
>
<Input id="deadline" type="date" {...register("deadline")} />
</PropertyRow>
<PropertyRow
label={t("taskFormDialog.fields.recurrence.label")}
error={translateError(
formState.errors.recurrenceIntervalUnit?.message
?? formState.errors.recurrenceIntervalCount?.message,
)}
>
<div className="flex items-center gap-2">
<Input
id="recurrenceIntervalCount"
type="number"
min={1}
className="w-16"
{...register("recurrenceIntervalCount", { valueAsNumber: true })}
/>
<Controller
name="recurrenceIntervalUnit"
control={control}
render={({ field }) => (
<Select
value={field.value ?? ""}
onValueChange={field.onChange}
>
<Option value="">{t("taskFormDialog.recurrenceIntervalUnits.none")}</Option>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Opening/rendering this recurrence selector throws because Radix Select.Item disallows value="". Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/components/tasks/TaskFormDialog.tsx, line 464:

<comment>Opening/rendering this recurrence selector throws because Radix `Select.Item` disallows `value=""`. Use a non-empty sentinel option and map it to null/empty before schema validation and mutation submission.</comment>

<file context>
@@ -393,6 +438,39 @@ export default function TaskFormDialog(props: Props) {
+                      value={field.value ?? ""}
+                      onValueChange={field.onChange}
+                    >
+                      <Option value="">{t("taskFormDialog.recurrenceIntervalUnits.none")}</Option>
+                      <Option value="DAY">{t("taskFormDialog.recurrenceIntervalUnits.day")}</Option>
+                      <Option value="WEEK">{t("taskFormDialog.recurrenceIntervalUnits.week")}</Option>
</file context>

<Option value="DAY">{t("taskFormDialog.recurrenceIntervalUnits.day")}</Option>
<Option value="WEEK">{t("taskFormDialog.recurrenceIntervalUnits.week")}</Option>
<Option value="MONTH">{t("taskFormDialog.recurrenceIntervalUnits.month")}</Option>
<Option value="YEAR">{t("taskFormDialog.recurrenceIntervalUnits.year")}</Option>
</Select>
)}
/>
</div>
</PropertyRow>
</div>
</DialogContent>
<DialogFooter>
Expand Down
13 changes: 13 additions & 0 deletions apps/console/src/components/tasks/TasksCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
IconCircleCheck,
IconCircleProgress,
IconPencil,
IconRotateCw,
IconTrashCan,
PriorityLevel,
TabBadge,
Expand Down Expand Up @@ -495,6 +496,8 @@ const fragment = graphql`
description
timeEstimate
deadline
recurrenceIntervalUnit
recurrenceIntervalCount
canUpdate: permission(action: "core:task:update")
canDelete: permission(action: "core:task:delete")
assignedTo {
Expand Down Expand Up @@ -621,6 +624,16 @@ function TaskRow(props: TaskRowProps) {
<div className="flex items-center gap-2 pt-[2px]">
<PriorityLevel level={task.priority} />
<TaskStateIcon state={displayState} />
{task.recurrenceIntervalUnit && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The recurring badge icon uses a <span title="..."> to convey recurrence information, but screen readers won't reliably announce this. The embedded <IconRotateCw /> SVG likely has no accessible name (no aria-label, role="img"), and the <span> isn't keyboard-focusable. Consider adding aria-label directly on the icon or using a visually-hidden text element for screen reader users, matching the pattern used by <Button> with title in this same component.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/components/tasks/TasksCard.tsx, line 627:

<comment>The recurring badge icon uses a `<span title="...">` to convey recurrence information, but screen readers won't reliably announce this. The embedded `<IconRotateCw />` SVG likely has no accessible name (no `aria-label`, `role="img"`), and the `<span>` isn't keyboard-focusable. Consider adding `aria-label` directly on the icon or using a visually-hidden text element for screen reader users, matching the pattern used by `<Button>` with `title` in this same component.</comment>

<file context>
@@ -621,6 +624,16 @@ function TaskRow(props: TaskRowProps) {
           <div className="flex items-center gap-2 pt-[2px]">
             <PriorityLevel level={task.priority} />
             <TaskStateIcon state={displayState} />
+            {task.recurrenceIntervalUnit && (
+              <span
+                title={t("tasksCard.recurringBadge.tooltip", {
</file context>

<span
title={t("tasksCard.recurringBadge.tooltip", {
count: task.recurrenceIntervalCount ?? 1,
unit: t(`taskFormDialog.recurrenceIntervalUnits.${task.recurrenceIntervalUnit.toLowerCase()}`),
})}
>
<IconRotateCw size={14} className="text-txt-secondary" />
</span>
)}
</div>
<div className="text-sm space-y-1 flex-1">
<h2 className="font-medium">{task.name}</h2>
Expand Down
Loading