Fix task display

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-01-05 12:58:42 +01:00
parent e077b5866f
commit 6ab5ace353
18 changed files with 216 additions and 655 deletions

View File

@@ -73,10 +73,7 @@ const createTaskSchema = z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
timeEstimate: z.string().optional().nullable(),
assignedToId: z.preprocess(
(val) => (val === "" || val == null ? undefined : val),
z.string({ required_error: "Assigned to is required" }).min(1, "Assigned to is required")
),
assignedToId: z.string().optional().nullable(),
measureId: z.preprocess(
(val) => (val === "" || val == null ? undefined : val),
z.string({ required_error: "Measure is required" }).min(1, "Measure is required")
@@ -88,7 +85,7 @@ const updateTaskSchema = z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
timeEstimate: z.string().optional().nullable(),
assignedToId: z.string().optional(),
assignedToId: z.string().optional().nullable(),
measureId: z.string().optional(),
deadline: z.string().optional().nullable(),
});
@@ -137,6 +134,7 @@ export default function TaskFormDialog(props: Props) {
description: data.description || null,
timeEstimate: data.timeEstimate || null,
deadline: formatDatetime(data.deadline) ?? null,
assignedToId: data.assignedToId ?? null,
},
},
});
@@ -149,7 +147,7 @@ export default function TaskFormDialog(props: Props) {
description: data.description || null,
timeEstimate: data.timeEstimate || null,
deadline: formatDatetime(data.deadline) ?? null,
assignedToId: data.assignedToId,
assignedToId: data.assignedToId || null,
measureId: data.measureId,
},
connections: [props.connection!],
@@ -165,7 +163,6 @@ export default function TaskFormDialog(props: Props) {
dialogRef.current?.close();
});
const showMeasure = !props.measureId && !isUpdating;
const isCreating = !isUpdating;
return (
<Dialog
@@ -198,18 +195,17 @@ export default function TaskFormDialog(props: Props) {
{/* Properties form */}
<div className="py-5 px-6 bg-subtle">
<Label>{__("Properties")}</Label>
{isCreating && (
<PropertyRow
label={__("Assigned to")}
error={formState.errors.assignedToId?.message}
>
<PeopleSelectField
name="assignedToId"
control={control}
organizationId={organizationId}
/>
</PropertyRow>
)}
<PropertyRow
label={__("Assigned to")}
error={formState.errors.assignedToId?.message}
>
<PeopleSelectField
name="assignedToId"
control={control}
organizationId={organizationId}
optional={true}
/>
</PropertyRow>
{showMeasure && (
<PropertyRow
label={__("Measure")}

View File

@@ -1,9 +1,8 @@
import { promisifyMutation } from "@probo/helpers";
import { promisifyMutation, formatDate, formatDuration } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
Avatar,
Card,
DropdownItem,
IconArrowCornerDownLeft,
@@ -40,6 +39,8 @@ type Props = {
name: string;
state: "TODO" | "DONE";
description?: string | null;
timeEstimate?: string | null;
deadline?: string | null;
measure?: {
id: string;
name: string;
@@ -128,7 +129,7 @@ export default function TasksCard({ tasks, connectionId }: Props) {
}
type TaskRowProps = {
task: ItemOf<Props["tasks"]> & TaskFormDialogFragment$key;
task: ItemOf<Props["tasks"]>;
connectionId: string;
hasAnyAction: boolean;
};
@@ -207,30 +208,48 @@ function TaskRow(props: TaskRowProps) {
<TaskStateIcon state={props.task.state} />
</button>
</div>
<div className="text-sm space-y-1">
<div className="text-sm space-y-1 flex-1">
<h2 className="font-medium">{props.task.name}</h2>
{props.task.measure && (
<p className="text-txt-secondary flex items-center gap-2">
<IconArrowCornerDownLeft className="scale-x-[-1]" size={16} />
<Link
className="hover:underline"
to={`/organizations/${organizationId}/measures/${props.task.measure?.id}`}
>
{props.task.measure?.name}
</Link>
{props.task.description && (
<p className="text-txt-secondary whitespace-pre-wrap break-words">
{props.task.description}
</p>
)}
<div className="flex flex-wrap items-center gap-3 text-txt-secondary text-xs">
{props.task.measure && (
<span className="flex items-center gap-1">
<IconArrowCornerDownLeft className="scale-x-[-1]" size={14} />
<Link
className="hover:underline"
to={`/organizations/${organizationId}/measures/${props.task.measure?.id}`}
>
{props.task.measure?.name}
</Link>
</span>
)}
{props.task.timeEstimate && (
<span>{formatDuration(props.task.timeEstimate, __)}</span>
)}
{props.task.deadline && (
<time dateTime={props.task.deadline}>
{formatDate(props.task.deadline)}
</time>
)}
</div>
</div>
</div>
{props.task.assignedTo?.fullName && (
<div className="text-sm text-txt-secondary ml-auto mr-8">
<Link
className="hover:underline"
to={`/organizations/${organizationId}/people/${props.task.assignedTo.id}`}
>
{props.task.assignedTo.fullName}
</Link>
</div>
)}
<div className="flex gap-2 items-center">
{isUpdating && <Spinner size={16} />}
{props.task.assignedTo && (
<Link
to={`/organizations/${organizationId}/people/${props.task.assignedTo?.id}`}
>
<Avatar name={props.task.assignedTo?.fullName ?? ""} />
</Link>
)}
{props.hasAnyAction && (
<ActionDropdown>
{isAuthorized("Task", "updateTask") && (

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<1cc9998f8dcbb02ac977744023d372d6>>
* @generated SignedSource<<1d9eed2918f5f58de18ef4ea5f0058b7>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type TaskState = "DONE" | "TODO";
export type UpdateTaskInput = {
assignedToId?: string | null | undefined;
deadline?: any | null | undefined;
description?: string | null | undefined;
name?: string | null | undefined;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<cff26152bf46e7734e8ae5f5671b47b6>>
* @generated SignedSource<<40ebf241dc89f4b4b1705e4a2fabb935>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -296,12 +296,12 @@ return {
]
},
"params": {
"cacheID": "a0b3367a3288c70f0bbcde73ec237bee",
"cacheID": "72cbeaa7667c477f85e5c3ecb09b7bf4",
"id": null,
"metadata": {},
"name": "TaskGraphQuery",
"operationKind": "query",
"text": "query TaskGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...TasksPageFragment\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksPageFragment on Organization {\n tasks(first: 500) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
"text": "query TaskGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...TasksPageFragment\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksPageFragment on Organization {\n tasks(first: 500) {\n edges {\n node {\n id\n name\n state\n description\n timeEstimate\n deadline\n ...TaskFormDialogFragment\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();

View File

@@ -20,6 +20,8 @@ const tasksQuery = graphql`
name
state
description
timeEstimate
deadline
...TaskFormDialogFragment
assignedTo {
id

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<f2e7ca902ae7ee08f260f27a0fefa043>>
* @generated SignedSource<<f1100222ab7c7aeff0f7643640ecc23c>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -25,10 +25,12 @@ export type MeasureTasksTabQuery$data = {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly deadline: any | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly name: string;
readonly state: TaskState;
readonly timeEstimate: any | null | undefined;
readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">;
};
}>;
@@ -84,6 +86,20 @@ v5 = {
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"concreteType": "People",
@@ -102,21 +118,21 @@ v6 = {
],
"storageKey": null
},
v7 = {
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v8 = {
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v9 = {
v11 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
@@ -141,7 +157,7 @@ v9 = {
],
"storageKey": null
},
v10 = {
v12 = {
"kind": "ClientExtension",
"selections": [
{
@@ -153,7 +169,7 @@ v10 = {
}
]
},
v11 = [
v13 = [
{
"kind": "Literal",
"name": "first",
@@ -207,22 +223,24 @@ return {
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "TaskFormDialogFragment"
},
(v6/*: any*/),
(v7/*: any*/)
(v8/*: any*/),
(v9/*: any*/)
],
"storageKey": null
},
(v8/*: any*/)
(v10/*: any*/)
],
"storageKey": null
},
(v9/*: any*/),
(v10/*: any*/)
(v11/*: any*/),
(v12/*: any*/)
],
"storageKey": null
}
@@ -251,14 +269,14 @@ return {
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v9/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v11/*: any*/),
"args": (v13/*: any*/),
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "tasks",
@@ -284,21 +302,9 @@ return {
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -311,22 +317,22 @@ return {
],
"storageKey": null
},
(v7/*: any*/)
(v9/*: any*/)
],
"storageKey": null
},
(v8/*: any*/)
(v10/*: any*/)
],
"storageKey": null
},
(v9/*: any*/),
(v10/*: any*/)
(v11/*: any*/),
(v12/*: any*/)
],
"storageKey": "tasks(first:100)"
},
{
"alias": null,
"args": (v11/*: any*/),
"args": (v13/*: any*/),
"filters": null,
"handle": "connection",
"key": "Measure__tasks",
@@ -343,7 +349,7 @@ return {
]
},
"params": {
"cacheID": "bc71d01128d96026a1e630b96e649409",
"cacheID": "7988e2a18d5929fe99d9cef8de244627",
"id": null,
"metadata": {
"connection": [
@@ -360,11 +366,11 @@ return {
},
"name": "MeasureTasksTabQuery",
"operationKind": "query",
"text": "query MeasureTasksTabQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n"
"text": "query MeasureTasksTabQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n timeEstimate\n deadline\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n"
}
};
})();
(node as any).hash = "12445eb032af3bc00fea9edce6f427e2";
(node as any).hash = "358a07bb3e13ea9828d0703541caf894";
export default node;

View File

@@ -39,6 +39,8 @@ const tasksFragment = graphql`
name
state
description
timeEstimate
deadline
...TaskFormDialogFragment
measure {
id

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<8bba8d318de0f4b0d28a8fba8b8e0bdf>>
* @generated SignedSource<<f6e71f80c25925eb372e360c1b9ec122>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -21,6 +21,7 @@ export type TasksPageFragment$data = {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly deadline: any | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly measure: {
@@ -29,6 +30,7 @@ export type TasksPageFragment$data = {
} | null | undefined;
readonly name: string;
readonly state: TaskState;
readonly timeEstimate: any | null | undefined;
readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">;
};
}>;
@@ -168,6 +170,20 @@ return {
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
@@ -286,6 +302,6 @@ return {
};
})();
(node as any).hash = "84864638db9ddf53e9ae7da7952803af";
(node as any).hash = "466d3b82119334991a0714bb0b7cd6d0";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<46a3b0baa64ec5a6a1c6d289a3737ade>>
* @generated SignedSource<<e765e52f0386640e282459c13a3e9769>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -369,16 +369,16 @@ return {
]
},
"params": {
"cacheID": "58db4c11a870b12c21eef1c6f24271b0",
"cacheID": "d31450ee0ddd09b0e08b43c9f7728f5f",
"id": null,
"metadata": {},
"name": "TasksPageFragment_query",
"operationKind": "query",
"text": "query TasksPageFragment_query(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 500\n $last: Int = null\n $order: TaskOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...TasksPageFragment_16fISc\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksPageFragment_16fISc on Organization {\n tasks(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
"text": "query TasksPageFragment_query(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 500\n $last: Int = null\n $order: TaskOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...TasksPageFragment_16fISc\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n\nfragment TasksPageFragment_16fISc on Organization {\n tasks(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n state\n description\n timeEstimate\n deadline\n ...TaskFormDialogFragment\n measure {\n id\n name\n }\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "84864638db9ddf53e9ae7da7952803af";
(node as any).hash = "466d3b82119334991a0714bb0b7cd6d0";
export default node;