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 { 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`
query ControlOverviewPageQuery($controlId: ID!) {
control: node(id: $controlId) {
@@ -74,6 +111,7 @@ const controlOverviewPageQuery = graphql`
name
description
state
timeEstimate
version
evidences(first: 50)
@connection(key: "ControlOverviewPage_evidences") {
@@ -122,6 +160,7 @@ const createTaskMutation = graphql`
id
name
description
timeEstimate
state
}
}
@@ -269,6 +308,9 @@ function ControlOverviewPageContent({
const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false);
const [newTaskName, setNewTaskName] = useState("");
const [newTaskDescription, setNewTaskDescription] = useState("");
const [timeEstimateDays, setTimeEstimateDays] = useState("");
const [timeEstimateHours, setTimeEstimateHours] = useState("");
const [timeEstimateMinutes, setTimeEstimateMinutes] = useState("");
const [isDeleteTaskOpen, setIsDeleteTaskOpen] = useState(false);
const [taskToDelete, setTaskToDelete] = useState<{
@@ -325,6 +367,33 @@ function ControlOverviewPageContent({
[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(() => {
const handleDragEnter = (e: globalThis.DragEvent) => {
e.preventDefault();
@@ -415,6 +484,9 @@ function ControlOverviewPageContent({
return;
}
// Convert the time estimate components to ISO 8601 format
const isoTimeEstimate = convertToISODuration();
createTask({
variables: {
connections: [`${data.control.tasks?.__id}`],
@@ -422,6 +494,7 @@ function ControlOverviewPageContent({
controlId: data.control.id,
name: newTaskName,
description: newTaskDescription,
timeEstimate: isoTimeEstimate,
},
},
onCompleted: () => {
@@ -431,6 +504,9 @@ function ControlOverviewPageContent({
});
setNewTaskName("");
setNewTaskDescription("");
setTimeEstimateDays("");
setTimeEstimateHours("");
setTimeEstimateMinutes("");
setIsCreateTaskOpen(false);
},
onError: (error) => {
@@ -804,6 +880,72 @@ function ControlOverviewPageContent({
placeholder="Enter task description"
/>
</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>
<DialogFooter>
<Button
@@ -903,15 +1045,16 @@ function ControlOverviewPageContent({
>
{task?.name}
</h3>
{task?.description && (
{task?.timeEstimate && (
<p
className={`text-xs mt-1 ${
className={`text-xs mt-1 flex items-center ${
task?.state === "DONE"
? "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>
)}
</div>

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<1454591ad65fc1aa76b9ac37604383ce>>
* @generated SignedSource<<502600204c0e5180095f00fe3372ea5a>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -14,6 +14,7 @@ export type CreateTaskInput = {
controlId: string;
description: string;
name: string;
timeEstimate: any;
};
export type ControlOverviewPageCreateTaskMutation$variables = {
connections: ReadonlyArray<string>;
@@ -27,6 +28,7 @@ export type ControlOverviewPageCreateTaskMutation$data = {
readonly id: string;
readonly name: string;
readonly state: TaskState;
readonly timeEstimate: any;
};
};
};
@@ -91,6 +93,13 @@ v3 = {
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -170,16 +179,16 @@ return {
]
},
"params": {
"cacheID": "a181d85ad48dde4a694a7def2700ee96",
"cacheID": "ed1842681cbb14392603c1b73e82b6f9",
"id": null,
"metadata": {},
"name": "ControlOverviewPageCreateTaskMutation",
"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;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5f28c9407a05834be16d66330e268caa>>
* @generated SignedSource<<1a0065809cf4bdeff6f4bb7ff087cdf7>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -45,6 +45,7 @@ export type ControlOverviewPageQuery$data = {
readonly id: string;
readonly name: string;
readonly state: TaskState;
readonly timeEstimate: any;
readonly version: number;
};
}>;
@@ -117,24 +118,31 @@ v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"name": "timeEstimate",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"name": "version",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"name": "__typename",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
@@ -159,7 +167,7 @@ v11 = {
],
"storageKey": null
},
v12 = {
v13 = {
"kind": "ClientExtension",
"selections": [
{
@@ -171,7 +179,7 @@ v12 = {
}
]
},
v13 = [
v14 = [
{
"alias": null,
"args": null,
@@ -218,25 +226,25 @@ v13 = [
"name": "createdAt",
"storageKey": null
},
(v9/*: any*/)
(v10/*: any*/)
],
"storageKey": null
},
(v10/*: any*/)
(v11/*: any*/)
],
"storageKey": null
},
(v11/*: any*/),
(v12/*: any*/)
(v12/*: any*/),
(v13/*: any*/)
],
v14 = [
v15 = [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
v15 = [
v16 = [
{
"kind": "Literal",
"name": "first",
@@ -296,6 +304,7 @@ return {
(v4/*: any*/),
(v5/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
{
"alias": "evidences",
"args": null,
@@ -303,19 +312,19 @@ return {
"kind": "LinkedField",
"name": "__ControlOverviewPage_evidences_connection",
"plural": false,
"selections": (v13/*: any*/),
"selections": (v14/*: any*/),
"storageKey": null
},
(v9/*: any*/)
(v10/*: any*/)
],
"storageKey": null
},
(v10/*: any*/)
(v11/*: any*/)
],
"storageKey": null
},
(v11/*: any*/),
(v12/*: any*/)
(v12/*: any*/),
(v13/*: any*/)
],
"storageKey": null
}
@@ -344,7 +353,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v9/*: any*/),
(v10/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
@@ -356,7 +365,7 @@ return {
(v7/*: any*/),
{
"alias": null,
"args": (v14/*: any*/),
"args": (v15/*: any*/),
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "tasks",
@@ -383,41 +392,42 @@ return {
(v4/*: any*/),
(v5/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
{
"alias": null,
"args": (v15/*: any*/),
"args": (v16/*: any*/),
"concreteType": "EvidenceConnection",
"kind": "LinkedField",
"name": "evidences",
"plural": false,
"selections": (v13/*: any*/),
"selections": (v14/*: any*/),
"storageKey": "evidences(first:50)"
},
{
"alias": null,
"args": (v15/*: any*/),
"args": (v16/*: any*/),
"filters": null,
"handle": "connection",
"key": "ControlOverviewPage_evidences",
"kind": "LinkedHandle",
"name": "evidences"
},
(v9/*: any*/)
(v10/*: any*/)
],
"storageKey": null
},
(v10/*: any*/)
(v11/*: any*/)
],
"storageKey": null
},
(v11/*: any*/),
(v12/*: any*/)
(v12/*: any*/),
(v13/*: any*/)
],
"storageKey": "tasks(first:100)"
},
{
"alias": null,
"args": (v14/*: any*/),
"args": (v15/*: any*/),
"filters": null,
"handle": "connection",
"key": "ControlOverviewPage_tasks",
@@ -434,7 +444,7 @@ return {
]
},
"params": {
"cacheID": "1dc4bdedef3b39fd2e441f5259f83d29",
"cacheID": "bd9679f5980ac0eed07f28e4f0cf4f27",
"id": null,
"metadata": {
"connection": [
@@ -457,11 +467,11 @@ return {
},
"name": "ControlOverviewPageQuery",
"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;

View File

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

View File

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

View File

@@ -28,3 +28,6 @@ models:
CursorKey:
model:
- "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 Datetime
scalar Upload
scalar Duration
interface Node {
id: ID!
@@ -261,6 +262,7 @@ type Task implements Node {
name: String!
description: String!
state: TaskState!
timeEstimate: Duration!
evidences(
first: Int
@@ -485,6 +487,7 @@ input CreateTaskInput {
controlId: ID!
name: String!
description: String!
timeEstimate: Duration!
}
type CreateTaskPayload {

View File

@@ -290,14 +290,15 @@ type ComplexityRoot struct {
}
Task struct {
CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
Evidences func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
ID func(childComplexity int) int
Name func(childComplexity int) int
State func(childComplexity int) int
UpdatedAt func(childComplexity int) int
Version func(childComplexity int) int
CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
Evidences func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
ID func(childComplexity int) int
Name func(childComplexity int) int
State func(childComplexity int) int
TimeEstimate func(childComplexity int) int
UpdatedAt func(childComplexity int) int
Version func(childComplexity int) int
}
TaskConnection struct {
@@ -1495,6 +1496,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
if e.complexity.Task.UpdatedAt == nil {
break
@@ -1904,6 +1912,7 @@ scalar CursorKey
scalar Void
scalar Datetime
scalar Upload
scalar Duration
interface Node {
id: ID!
@@ -2150,6 +2159,7 @@ type Task implements Node {
name: String!
description: String!
state: TaskState!
timeEstimate: Duration!
evidences(
first: Int
@@ -2374,6 +2384,7 @@ input CreateTaskInput {
controlId: ID!
name: String!
description: String!
timeEstimate: Duration!
}
type CreateTaskPayload {
@@ -9313,6 +9324,44 @@ func (ec *executionContext) fieldContext_Task_state(_ context.Context, field gra
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) {
fc, err := ec.fieldContext_Task_evidences(ctx, field)
if err != nil {
@@ -9611,6 +9660,8 @@ func (ec *executionContext) fieldContext_TaskEdge_node(_ context.Context, field
return ec.fieldContext_Task_description(ctx, field)
case "state":
return ec.fieldContext_Task_state(ctx, field)
case "timeEstimate":
return ec.fieldContext_Task_timeEstimate(ctx, field)
case "evidences":
return ec.fieldContext_Task_evidences(ctx, field)
case "createdAt":
@@ -9953,6 +10004,8 @@ func (ec *executionContext) fieldContext_UpdateTaskPayload_task(_ context.Contex
return ec.fieldContext_Task_description(ctx, field)
case "state":
return ec.fieldContext_Task_state(ctx, field)
case "timeEstimate":
return ec.fieldContext_Task_timeEstimate(ctx, field)
case "evidences":
return ec.fieldContext_Task_evidences(ctx, field)
case "createdAt":
@@ -12966,7 +13019,7 @@ func (ec *executionContext) unmarshalInputCreateTaskInput(ctx context.Context, o
asMap[k] = v
}
fieldsInOrder := [...]string{"controlId", "name", "description"}
fieldsInOrder := [...]string{"controlId", "name", "description", "timeEstimate"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -12994,6 +13047,13 @@ func (ec *executionContext) unmarshalInputCreateTaskInput(ctx context.Context, o
return it, err
}
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 {
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":
field := field
@@ -17547,6 +17612,21 @@ func (ec *executionContext) marshalNDeleteVendorPayload2ᚖgithubᚗcomᚋgetpro
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 {
if v == nil {
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 {
return &Task{
ID: t.ID,
Name: t.Name,
Description: t.Description,
State: t.State,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
Version: t.Version,
ID: t.ID,
Name: t.Name,
Description: t.Description,
State: t.State,
TimeEstimate: t.TimeEstimate,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
Version: t.Version,
}
}

View File

@@ -106,9 +106,10 @@ type CreatePolicyPayload struct {
}
type CreateTaskInput struct {
ControlID gid.GID `json:"controlId"`
Name string `json:"name"`
Description string `json:"description"`
ControlID gid.GID `json:"controlId"`
Name string `json:"name"`
Description string `json:"description"`
TimeEstimate time.Duration `json:"timeEstimate"`
}
type CreateTaskPayload struct {
@@ -329,14 +330,15 @@ type Session struct {
}
type Task struct {
ID gid.GID `json:"id"`
Version int `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
State coredata.TaskState `json:"state"`
Evidences *EvidenceConnection `json:"evidences"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
Version int `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
State coredata.TaskState `json:"state"`
TimeEstimate time.Duration `json:"timeEstimate"`
Evidences *EvidenceConnection `json:"evidences"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
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())
task, err := svc.Tasks.Create(ctx, probo.CreateTaskRequest{
ControlID: input.ControlID,
Name: input.Name,
Description: input.Description,
ControlID: input.ControlID,
Name: input.Name,
Description: input.Description,
TimeEstimate: input.TimeEstimate,
})
if err != nil {
return nil, fmt.Errorf("cannot create task: %w", err)