Add task time estimate

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-13 09:14:08 +01:00
parent 3255e32eac
commit 376edd927d
11 changed files with 366 additions and 101 deletions

View File

@@ -56,6 +56,43 @@ import type { ControlOverviewPageUploadEvidenceMutation as ControlOverviewPageUp
import type { ControlOverviewPageDeleteEvidenceMutation as ControlOverviewPageDeleteEvidenceMutationType } from "./__generated__/ControlOverviewPageDeleteEvidenceMutation.graphql"; import type { ControlOverviewPageDeleteEvidenceMutation as ControlOverviewPageDeleteEvidenceMutationType } from "./__generated__/ControlOverviewPageDeleteEvidenceMutation.graphql";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
// Function to format ISO8601 duration to human-readable format
const formatDuration = (isoDuration: string): string => {
if (!isoDuration || !isoDuration.startsWith("P")) {
return isoDuration;
}
try {
const durationRegex =
/P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/;
const matches = isoDuration.match(durationRegex);
if (!matches) return isoDuration;
const years = matches[1] ? parseInt(matches[1]) : 0;
const months = matches[2] ? parseInt(matches[2]) : 0;
const days = matches[3] ? parseInt(matches[3]) : 0;
const hours = matches[4] ? parseInt(matches[4]) : 0;
const minutes = matches[5] ? parseInt(matches[5]) : 0;
const seconds = matches[6] ? parseInt(matches[6]) : 0;
const parts = [];
if (years) parts.push(`${years} ${years === 1 ? "year" : "years"}`);
if (months) parts.push(`${months} ${months === 1 ? "month" : "months"}`);
if (days) parts.push(`${days} ${days === 1 ? "day" : "days"}`);
if (hours) parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`);
if (minutes)
parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`);
if (seconds)
parts.push(`${seconds} ${seconds === 1 ? "second" : "seconds"}`);
return parts.length > 0 ? parts.join(", ") : "No duration";
} catch (error) {
console.error("Error parsing duration:", error);
return isoDuration;
}
};
const controlOverviewPageQuery = graphql` const controlOverviewPageQuery = graphql`
query ControlOverviewPageQuery($controlId: ID!) { query ControlOverviewPageQuery($controlId: ID!) {
control: node(id: $controlId) { control: node(id: $controlId) {
@@ -74,6 +111,7 @@ const controlOverviewPageQuery = graphql`
name name
description description
state state
timeEstimate
version version
evidences(first: 50) evidences(first: 50)
@connection(key: "ControlOverviewPage_evidences") { @connection(key: "ControlOverviewPage_evidences") {
@@ -122,6 +160,7 @@ const createTaskMutation = graphql`
id id
name name
description description
timeEstimate
state state
} }
} }
@@ -269,6 +308,9 @@ function ControlOverviewPageContent({
const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false);
const [newTaskName, setNewTaskName] = useState(""); const [newTaskName, setNewTaskName] = useState("");
const [newTaskDescription, setNewTaskDescription] = useState(""); const [newTaskDescription, setNewTaskDescription] = useState("");
const [timeEstimateDays, setTimeEstimateDays] = useState("");
const [timeEstimateHours, setTimeEstimateHours] = useState("");
const [timeEstimateMinutes, setTimeEstimateMinutes] = useState("");
const [isDeleteTaskOpen, setIsDeleteTaskOpen] = useState(false); const [isDeleteTaskOpen, setIsDeleteTaskOpen] = useState(false);
const [taskToDelete, setTaskToDelete] = useState<{ const [taskToDelete, setTaskToDelete] = useState<{
@@ -325,6 +367,33 @@ function ControlOverviewPageContent({
[tasks] [tasks]
); );
// Function to convert days, hours, and minutes to ISO 8601 duration format
const convertToISODuration = useCallback(() => {
let duration = "P";
if (timeEstimateDays && parseInt(timeEstimateDays) > 0) {
duration += `${parseInt(timeEstimateDays)}D`;
}
if (
(timeEstimateHours && parseInt(timeEstimateHours) > 0) ||
(timeEstimateMinutes && parseInt(timeEstimateMinutes) > 0)
) {
duration += "T";
if (timeEstimateHours && parseInt(timeEstimateHours) > 0) {
duration += `${parseInt(timeEstimateHours)}H`;
}
if (timeEstimateMinutes && parseInt(timeEstimateMinutes) > 0) {
duration += `${parseInt(timeEstimateMinutes)}M`;
}
}
// Return empty string if no time components were provided
return duration === "P" ? "" : duration;
}, [timeEstimateDays, timeEstimateHours, timeEstimateMinutes]);
useEffect(() => { useEffect(() => {
const handleDragEnter = (e: globalThis.DragEvent) => { const handleDragEnter = (e: globalThis.DragEvent) => {
e.preventDefault(); e.preventDefault();
@@ -415,6 +484,9 @@ function ControlOverviewPageContent({
return; return;
} }
// Convert the time estimate components to ISO 8601 format
const isoTimeEstimate = convertToISODuration();
createTask({ createTask({
variables: { variables: {
connections: [`${data.control.tasks?.__id}`], connections: [`${data.control.tasks?.__id}`],
@@ -422,6 +494,7 @@ function ControlOverviewPageContent({
controlId: data.control.id, controlId: data.control.id,
name: newTaskName, name: newTaskName,
description: newTaskDescription, description: newTaskDescription,
timeEstimate: isoTimeEstimate,
}, },
}, },
onCompleted: () => { onCompleted: () => {
@@ -431,6 +504,9 @@ function ControlOverviewPageContent({
}); });
setNewTaskName(""); setNewTaskName("");
setNewTaskDescription(""); setNewTaskDescription("");
setTimeEstimateDays("");
setTimeEstimateHours("");
setTimeEstimateMinutes("");
setIsCreateTaskOpen(false); setIsCreateTaskOpen(false);
}, },
onError: (error) => { onError: (error) => {
@@ -804,6 +880,72 @@ function ControlOverviewPageContent({
placeholder="Enter task description" placeholder="Enter task description"
/> />
</div> </div>
<div className="space-y-2">
<label
htmlFor="timeEstimate"
className="text-sm font-medium"
>
Time Estimate (optional)
</label>
<div className="grid grid-cols-3 gap-4">
<div>
<label
htmlFor="days"
className="text-xs text-gray-500 block mb-1"
>
Days
</label>
<Input
id="days"
type="number"
min="0"
value={timeEstimateDays}
onChange={(e) =>
setTimeEstimateDays(e.target.value)
}
placeholder="0"
/>
</div>
<div>
<label
htmlFor="hours"
className="text-xs text-gray-500 block mb-1"
>
Hours
</label>
<Input
id="hours"
type="number"
min="0"
max="23"
value={timeEstimateHours}
onChange={(e) =>
setTimeEstimateHours(e.target.value)
}
placeholder="0"
/>
</div>
<div>
<label
htmlFor="minutes"
className="text-xs text-gray-500 block mb-1"
>
Minutes
</label>
<Input
id="minutes"
type="number"
min="0"
max="59"
value={timeEstimateMinutes}
onChange={(e) =>
setTimeEstimateMinutes(e.target.value)
}
placeholder="0"
/>
</div>
</div>
</div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button
@@ -903,15 +1045,16 @@ function ControlOverviewPageContent({
> >
{task?.name} {task?.name}
</h3> </h3>
{task?.description && ( {task?.timeEstimate && (
<p <p
className={`text-xs mt-1 ${ className={`text-xs mt-1 flex items-center ${
task?.state === "DONE" task?.state === "DONE"
? "text-gray-400 line-through" ? "text-gray-400 line-through"
: "text-gray-500" : "text-blue-500"
}`} }`}
> >
{task.description} <span className="inline-block w-4 h-4 mr-1">⏱️</span>
<span>{formatDuration(task.timeEstimate)}</span>
</p> </p>
)} )}
</div> </div>

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<1454591ad65fc1aa76b9ac37604383ce>> * @generated SignedSource<<502600204c0e5180095f00fe3372ea5a>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -14,6 +14,7 @@ export type CreateTaskInput = {
controlId: string; controlId: string;
description: string; description: string;
name: string; name: string;
timeEstimate: any;
}; };
export type ControlOverviewPageCreateTaskMutation$variables = { export type ControlOverviewPageCreateTaskMutation$variables = {
connections: ReadonlyArray<string>; connections: ReadonlyArray<string>;
@@ -27,6 +28,7 @@ export type ControlOverviewPageCreateTaskMutation$data = {
readonly id: string; readonly id: string;
readonly name: string; readonly name: string;
readonly state: TaskState; readonly state: TaskState;
readonly timeEstimate: any;
}; };
}; };
}; };
@@ -91,6 +93,13 @@ v3 = {
"name": "description", "name": "description",
"storageKey": null "storageKey": null
}, },
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -170,16 +179,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "a181d85ad48dde4a694a7def2700ee96", "cacheID": "ed1842681cbb14392603c1b73e82b6f9",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "ControlOverviewPageCreateTaskMutation", "name": "ControlOverviewPageCreateTaskMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation ControlOverviewPageCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n state\n }\n }\n }\n}\n" "text": "mutation ControlOverviewPageCreateTaskMutation(\n $input: CreateTaskInput!\n) {\n createTask(input: $input) {\n taskEdge {\n node {\n id\n name\n description\n timeEstimate\n state\n }\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "3e4ccc4d984d30492fc65afc16e6930c"; (node as any).hash = "d2e06ccdb00312c0862187a18f374d23";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<5f28c9407a05834be16d66330e268caa>> * @generated SignedSource<<1a0065809cf4bdeff6f4bb7ff087cdf7>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -45,6 +45,7 @@ export type ControlOverviewPageQuery$data = {
readonly id: string; readonly id: string;
readonly name: string; readonly name: string;
readonly state: TaskState; readonly state: TaskState;
readonly timeEstimate: any;
readonly version: number; readonly version: number;
}; };
}>; }>;
@@ -117,24 +118,31 @@ v8 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "version", "name": "timeEstimate",
"storageKey": null "storageKey": null
}, },
v9 = { v9 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "version",
"storageKey": null "storageKey": null
}, },
v10 = { v10 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "cursor", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v11 = { v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v12 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "concreteType": "PageInfo",
@@ -159,7 +167,7 @@ v11 = {
], ],
"storageKey": null "storageKey": null
}, },
v12 = { v13 = {
"kind": "ClientExtension", "kind": "ClientExtension",
"selections": [ "selections": [
{ {
@@ -171,7 +179,7 @@ v12 = {
} }
] ]
}, },
v13 = [ v14 = [
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -218,25 +226,25 @@ v13 = [
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
(v9/*: any*/) (v10/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v10/*: any*/) (v11/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), (v12/*: any*/),
(v12/*: any*/) (v13/*: any*/)
], ],
v14 = [ v15 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 100 "value": 100
} }
], ],
v15 = [ v16 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -296,6 +304,7 @@ return {
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/),
{ {
"alias": "evidences", "alias": "evidences",
"args": null, "args": null,
@@ -303,19 +312,19 @@ return {
"kind": "LinkedField", "kind": "LinkedField",
"name": "__ControlOverviewPage_evidences_connection", "name": "__ControlOverviewPage_evidences_connection",
"plural": false, "plural": false,
"selections": (v13/*: any*/), "selections": (v14/*: any*/),
"storageKey": null "storageKey": null
}, },
(v9/*: any*/) (v10/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v10/*: any*/) (v11/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), (v12/*: any*/),
(v12/*: any*/) (v13/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -344,7 +353,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v9/*: any*/), (v10/*: any*/),
(v2/*: any*/), (v2/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
@@ -356,7 +365,7 @@ return {
(v7/*: any*/), (v7/*: any*/),
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v15/*: any*/),
"concreteType": "TaskConnection", "concreteType": "TaskConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "tasks", "name": "tasks",
@@ -383,41 +392,42 @@ return {
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/),
{ {
"alias": null, "alias": null,
"args": (v15/*: any*/), "args": (v16/*: any*/),
"concreteType": "EvidenceConnection", "concreteType": "EvidenceConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "evidences", "name": "evidences",
"plural": false, "plural": false,
"selections": (v13/*: any*/), "selections": (v14/*: any*/),
"storageKey": "evidences(first:50)" "storageKey": "evidences(first:50)"
}, },
{ {
"alias": null, "alias": null,
"args": (v15/*: any*/), "args": (v16/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "ControlOverviewPage_evidences", "key": "ControlOverviewPage_evidences",
"kind": "LinkedHandle", "kind": "LinkedHandle",
"name": "evidences" "name": "evidences"
}, },
(v9/*: any*/) (v10/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v10/*: any*/) (v11/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), (v12/*: any*/),
(v12/*: any*/) (v13/*: any*/)
], ],
"storageKey": "tasks(first:100)" "storageKey": "tasks(first:100)"
}, },
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v15/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "ControlOverviewPage_tasks", "key": "ControlOverviewPage_tasks",
@@ -434,7 +444,7 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "1dc4bdedef3b39fd2e441f5259f83d29", "cacheID": "bd9679f5980ac0eed07f28e4f0cf4f27",
"id": null, "id": null,
"metadata": { "metadata": {
"connection": [ "connection": [
@@ -457,11 +467,11 @@ return {
}, },
"name": "ControlOverviewPageQuery", "name": "ControlOverviewPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n version\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" "text": "query ControlOverviewPageQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n name\n description\n state\n importance\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n version\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "ff1a734556fc0d56e90ddae161a93408"; (node as any).hash = "81d519cc3ea015284326fa7d5052b25f";
export default node; export default node;

View File

@@ -29,15 +29,16 @@ import (
type ( type (
Task struct { Task struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
ControlID gid.GID `db:"control_id"` ControlID gid.GID `db:"control_id"`
Name string `db:"name"` Name string `db:"name"`
Description string `db:"description"` Description string `db:"description"`
State TaskState `db:"state"` State TaskState `db:"state"`
ContentRef string `db:"content_ref"` ContentRef string `db:"content_ref"`
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"` UpdatedAt time.Time `db:"updated_at"`
Version int `db:"version"` Version int `db:"version"`
TimeEstimate time.Duration `db:"time_estimate"`
} }
Tasks []*Task Tasks []*Task
@@ -47,6 +48,7 @@ type (
Name *string Name *string
Description *string Description *string
State *TaskState State *TaskState
TimeEstimate *time.Duration
} }
) )
@@ -66,6 +68,7 @@ SELECT
control_id, control_id,
name, name,
description, description,
time_estimate,
state, state,
content_ref, content_ref,
created_at, created_at,
@@ -115,7 +118,8 @@ INSERT INTO tasks (
created_at, created_at,
updated_at, updated_at,
version, version,
state state,
time_estimate
) )
VALUES ( VALUES (
@tenant_id, @tenant_id,
@@ -127,21 +131,23 @@ VALUES (
@created_at, @created_at,
@updated_at, @updated_at,
@version, @version,
@state @state,
@time_estimate
); );
` `
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(), "tenant_id": scope.GetTenantID(),
"task_id": t.ID, "task_id": t.ID,
"control_id": t.ControlID, "control_id": t.ControlID,
"name": t.Name, "name": t.Name,
"description": t.Description, "description": t.Description,
"content_ref": t.ContentRef, "content_ref": t.ContentRef,
"created_at": t.CreatedAt, "created_at": t.CreatedAt,
"updated_at": t.UpdatedAt, "updated_at": t.UpdatedAt,
"version": t.Version, "version": t.Version,
"state": t.State, "state": t.State,
"time_estimate": t.TimeEstimate,
} }
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
@@ -161,6 +167,7 @@ SELECT
name, name,
description, description,
state, state,
time_estimate,
content_ref, content_ref,
created_at, created_at,
updated_at, updated_at,
@@ -206,6 +213,7 @@ SET
name = COALESCE(@name, name), name = COALESCE(@name, name),
description = COALESCE(@description, description), description = COALESCE(@description, description),
state = COALESCE(@state, state), state = COALESCE(@state, state),
time_estimate = COALESCE(@time_estimate, time_estimate),
updated_at = @updated_at, updated_at = @updated_at,
version = version + 1 version = version + 1
WHERE WHERE
@@ -223,6 +231,7 @@ RETURNING
"name": params.Name, "name": params.Name,
"description": params.Description, "description": params.Description,
"state": params.State, "state": params.State,
"time_estimate": params.TimeEstimate,
"updated_at": time.Now(), "updated_at": time.Now(),
} }
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())

View File

@@ -31,10 +31,11 @@ type (
} }
CreateTaskRequest struct { CreateTaskRequest struct {
ControlID gid.GID ControlID gid.GID
Name string Name string
ContentRef string ContentRef string
Description string Description string
TimeEstimate time.Duration
} }
UpdateTaskRequest struct { UpdateTaskRequest struct {
@@ -43,6 +44,7 @@ type (
Name *string Name *string
Description *string Description *string
State *coredata.TaskState State *coredata.TaskState
TimeEstimate *time.Duration
} }
) )
@@ -58,14 +60,15 @@ func (s TaskService) Create(
control := &coredata.Control{} control := &coredata.Control{}
task := &coredata.Task{ task := &coredata.Task{
ID: taskID, ID: taskID,
ControlID: req.ControlID, ControlID: req.ControlID,
Name: req.Name, Name: req.Name,
ContentRef: req.ContentRef, ContentRef: req.ContentRef,
State: coredata.TaskStateTodo, State: coredata.TaskStateTodo,
Description: req.Description, Description: req.Description,
CreatedAt: now, TimeEstimate: req.TimeEstimate,
UpdatedAt: now, CreatedAt: now,
UpdatedAt: now,
} }
err = s.svc.pg.WithTx( err = s.svc.pg.WithTx(
@@ -99,6 +102,7 @@ func (s TaskService) Update(
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
State: req.State, State: req.State,
TimeEstimate: req.TimeEstimate,
} }
task := &coredata.Task{ID: req.ID} task := &coredata.Task{ID: req.ID}

View File

@@ -28,3 +28,6 @@ models:
CursorKey: CursorKey:
model: model:
- "github.com/getprobo/probo/pkg/server/api/console/v1/types.CursorKeyScalar" - "github.com/getprobo/probo/pkg/server/api/console/v1/types.CursorKeyScalar"
Duration:
model:
- "github.com/99designs/gqlgen/graphql.Duration"

View File

@@ -15,6 +15,7 @@ scalar CursorKey
scalar Void scalar Void
scalar Datetime scalar Datetime
scalar Upload scalar Upload
scalar Duration
interface Node { interface Node {
id: ID! id: ID!
@@ -261,6 +262,7 @@ type Task implements Node {
name: String! name: String!
description: String! description: String!
state: TaskState! state: TaskState!
timeEstimate: Duration!
evidences( evidences(
first: Int first: Int
@@ -485,6 +487,7 @@ input CreateTaskInput {
controlId: ID! controlId: ID!
name: String! name: String!
description: String! description: String!
timeEstimate: Duration!
} }
type CreateTaskPayload { type CreateTaskPayload {

View File

@@ -290,14 +290,15 @@ type ComplexityRoot struct {
} }
Task struct { Task struct {
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Description func(childComplexity int) int Description func(childComplexity int) int
Evidences func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int Evidences func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
ID func(childComplexity int) int ID func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
State func(childComplexity int) int State func(childComplexity int) int
UpdatedAt func(childComplexity int) int TimeEstimate func(childComplexity int) int
Version func(childComplexity int) int UpdatedAt func(childComplexity int) int
Version func(childComplexity int) int
} }
TaskConnection struct { TaskConnection struct {
@@ -1495,6 +1496,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Task.State(childComplexity), true return e.complexity.Task.State(childComplexity), true
case "Task.timeEstimate":
if e.complexity.Task.TimeEstimate == nil {
break
}
return e.complexity.Task.TimeEstimate(childComplexity), true
case "Task.updatedAt": case "Task.updatedAt":
if e.complexity.Task.UpdatedAt == nil { if e.complexity.Task.UpdatedAt == nil {
break break
@@ -1904,6 +1912,7 @@ scalar CursorKey
scalar Void scalar Void
scalar Datetime scalar Datetime
scalar Upload scalar Upload
scalar Duration
interface Node { interface Node {
id: ID! id: ID!
@@ -2150,6 +2159,7 @@ type Task implements Node {
name: String! name: String!
description: String! description: String!
state: TaskState! state: TaskState!
timeEstimate: Duration!
evidences( evidences(
first: Int first: Int
@@ -2374,6 +2384,7 @@ input CreateTaskInput {
controlId: ID! controlId: ID!
name: String! name: String!
description: String! description: String!
timeEstimate: Duration!
} }
type CreateTaskPayload { type CreateTaskPayload {
@@ -9313,6 +9324,44 @@ func (ec *executionContext) fieldContext_Task_state(_ context.Context, field gra
return fc, nil return fc, nil
} }
func (ec *executionContext) _Task_timeEstimate(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Task_timeEstimate(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.TimeEstimate, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(time.Duration)
fc.Result = res
return ec.marshalNDuration2timeᚐDuration(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Task_timeEstimate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Task",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Duration does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Task_evidences(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) { func (ec *executionContext) _Task_evidences(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Task_evidences(ctx, field) fc, err := ec.fieldContext_Task_evidences(ctx, field)
if err != nil { if err != nil {
@@ -9611,6 +9660,8 @@ func (ec *executionContext) fieldContext_TaskEdge_node(_ context.Context, field
return ec.fieldContext_Task_description(ctx, field) return ec.fieldContext_Task_description(ctx, field)
case "state": case "state":
return ec.fieldContext_Task_state(ctx, field) return ec.fieldContext_Task_state(ctx, field)
case "timeEstimate":
return ec.fieldContext_Task_timeEstimate(ctx, field)
case "evidences": case "evidences":
return ec.fieldContext_Task_evidences(ctx, field) return ec.fieldContext_Task_evidences(ctx, field)
case "createdAt": case "createdAt":
@@ -9953,6 +10004,8 @@ func (ec *executionContext) fieldContext_UpdateTaskPayload_task(_ context.Contex
return ec.fieldContext_Task_description(ctx, field) return ec.fieldContext_Task_description(ctx, field)
case "state": case "state":
return ec.fieldContext_Task_state(ctx, field) return ec.fieldContext_Task_state(ctx, field)
case "timeEstimate":
return ec.fieldContext_Task_timeEstimate(ctx, field)
case "evidences": case "evidences":
return ec.fieldContext_Task_evidences(ctx, field) return ec.fieldContext_Task_evidences(ctx, field)
case "createdAt": case "createdAt":
@@ -12966,7 +13019,7 @@ func (ec *executionContext) unmarshalInputCreateTaskInput(ctx context.Context, o
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"controlId", "name", "description"} fieldsInOrder := [...]string{"controlId", "name", "description", "timeEstimate"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -12994,6 +13047,13 @@ func (ec *executionContext) unmarshalInputCreateTaskInput(ctx context.Context, o
return it, err return it, err
} }
it.Description = data it.Description = data
case "timeEstimate":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("timeEstimate"))
data, err := ec.unmarshalNDuration2timeᚐDuration(ctx, v)
if err != nil {
return it, err
}
it.TimeEstimate = data
} }
} }
@@ -16040,6 +16100,11 @@ func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) atomic.AddUint32(&out.Invalids, 1)
} }
case "timeEstimate":
out.Values[i] = ec._Task_timeEstimate(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "evidences": case "evidences":
field := field field := field
@@ -17547,6 +17612,21 @@ func (ec *executionContext) marshalNDeleteVendorPayload2ᚖgithubᚗcomᚋgetpro
return ec._DeleteVendorPayload(ctx, sel, v) return ec._DeleteVendorPayload(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNDuration2timeᚐDuration(ctx context.Context, v any) (time.Duration, error) {
res, err := graphql.UnmarshalDuration(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNDuration2timeᚐDuration(ctx context.Context, sel ast.SelectionSet, v time.Duration) graphql.Marshaler {
res := graphql.MarshalDuration(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
}
return res
}
func (ec *executionContext) marshalNEvidence2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEvidence(ctx context.Context, sel ast.SelectionSet, v *types.Evidence) graphql.Marshaler { func (ec *executionContext) marshalNEvidence2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEvidence(ctx context.Context, sel ast.SelectionSet, v *types.Evidence) graphql.Marshaler {
if v == nil { if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {

View File

@@ -41,12 +41,13 @@ func NewTaskEdge(t *coredata.Task) *TaskEdge {
func NewTask(t *coredata.Task) *Task { func NewTask(t *coredata.Task) *Task {
return &Task{ return &Task{
ID: t.ID, ID: t.ID,
Name: t.Name, Name: t.Name,
Description: t.Description, Description: t.Description,
State: t.State, State: t.State,
CreatedAt: t.CreatedAt, TimeEstimate: t.TimeEstimate,
UpdatedAt: t.UpdatedAt, CreatedAt: t.CreatedAt,
Version: t.Version, UpdatedAt: t.UpdatedAt,
Version: t.Version,
} }
} }

View File

@@ -106,9 +106,10 @@ type CreatePolicyPayload struct {
} }
type CreateTaskInput struct { type CreateTaskInput struct {
ControlID gid.GID `json:"controlId"` ControlID gid.GID `json:"controlId"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
TimeEstimate time.Duration `json:"timeEstimate"`
} }
type CreateTaskPayload struct { type CreateTaskPayload struct {
@@ -329,14 +330,15 @@ type Session struct {
} }
type Task struct { type Task struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Version int `json:"version"` Version int `json:"version"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
State coredata.TaskState `json:"state"` State coredata.TaskState `json:"state"`
Evidences *EvidenceConnection `json:"evidences"` TimeEstimate time.Duration `json:"timeEstimate"`
CreatedAt time.Time `json:"createdAt"` Evidences *EvidenceConnection `json:"evidences"`
UpdatedAt time.Time `json:"updatedAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} }
func (Task) IsNode() {} func (Task) IsNode() {}

View File

@@ -207,9 +207,10 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTas
svc := r.GetTenantServiceIfAuthorized(ctx, input.ControlID.TenantID()) svc := r.GetTenantServiceIfAuthorized(ctx, input.ControlID.TenantID())
task, err := svc.Tasks.Create(ctx, probo.CreateTaskRequest{ task, err := svc.Tasks.Create(ctx, probo.CreateTaskRequest{
ControlID: input.ControlID, ControlID: input.ControlID,
Name: input.Name, Name: input.Name,
Description: input.Description, Description: input.Description,
TimeEstimate: input.TimeEstimate,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot create task: %w", err) return nil, fmt.Errorf("cannot create task: %w", err)