From 6be112e3408a15d926924af6ef933a468e98afef Mon Sep 17 00:00:00 2001 From: gearnode Date: Wed, 26 Feb 2025 15:58:51 +0100 Subject: [PATCH] Add delete task Signed-off-by: gearnode --- .../console/src/pages/ControlOverviewPage.tsx | 101 +++++++- ...lOverviewPageDeleteTaskMutation.graphql.ts | 132 ++++++++++ pkg/api/console/v1/schema.graphql | 9 + pkg/api/console/v1/schema/schema.go | 235 ++++++++++++++++++ pkg/api/console/v1/types/types.go | 8 + pkg/api/console/v1/v1_resolver.go | 12 + pkg/probo/coredata/task.go | 72 ++++++ pkg/probo/delete_task.go | 22 ++ 8 files changed, 580 insertions(+), 11 deletions(-) create mode 100644 apps/console/src/pages/__generated__/ControlOverviewPageDeleteTaskMutation.graphql.ts create mode 100644 pkg/probo/delete_task.go diff --git a/apps/console/src/pages/ControlOverviewPage.tsx b/apps/console/src/pages/ControlOverviewPage.tsx index f0799310e..c4b985030 100644 --- a/apps/console/src/pages/ControlOverviewPage.tsx +++ b/apps/console/src/pages/ControlOverviewPage.tsx @@ -7,7 +7,7 @@ import { useQueryLoader, useMutation, } from "react-relay"; -import { CheckCircle2, Plus } from "lucide-react"; +import { CheckCircle2, Plus, Trash2 } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; import { Button } from "@/components/ui/button"; @@ -26,6 +26,8 @@ import { Helmet } from "react-helmet-async"; import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql"; import type { ControlOverviewPageUpdateTaskStateMutation as ControlOverviewPageUpdateTaskStateMutationType } from "./__generated__/ControlOverviewPageUpdateTaskStateMutation.graphql"; import type { ControlOverviewPageCreateTaskMutation as ControlOverviewPageCreateTaskMutationType } from "./__generated__/ControlOverviewPageCreateTaskMutation.graphql"; +// The import below will be generated after the first run +// import type { ControlOverviewPageDeleteTaskMutation as ControlOverviewPageDeleteTaskMutationType } from "./__generated__/ControlOverviewPageDeleteTaskMutation.graphql"; const controlOverviewPageQuery = graphql` query ControlOverviewPageQuery($controlId: ID!) { @@ -83,6 +85,17 @@ const createTaskMutation = graphql` } `; +const deleteTaskMutation = graphql` + mutation ControlOverviewPageDeleteTaskMutation( + $input: DeleteTaskInput! + $connections: [ID!]! + ) { + deleteTask(input: $input) { + deletedTaskId @deleteEdge(connections: $connections) + } + } +`; + function ControlOverviewPageContent({ queryRef, }: { @@ -99,6 +112,8 @@ function ControlOverviewPageContent({ ); const [createTask] = useMutation(createTaskMutation); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [deleteTask] = useMutation(deleteTaskMutation); const control = data.control; const tasks = control?.tasks?.edges.map((edge) => edge?.node) ?? []; @@ -107,6 +122,13 @@ function ControlOverviewPageContent({ const [newTaskName, setNewTaskName] = useState(""); const [newTaskDescription, setNewTaskDescription] = useState(""); + // State for the delete task dialog + const [isDeleteTaskOpen, setIsDeleteTaskOpen] = useState(false); + const [taskToDelete, setTaskToDelete] = useState<{ + id: string; + name: string; + } | null>(null); + const handleTaskClick = (taskId: string, currentState: string) => { const newState = currentState === "DONE" ? "TODO" : "DONE"; @@ -192,6 +214,39 @@ function ControlOverviewPageContent({ }); }; + const handleDeleteTask = (taskId: string, taskName: string) => { + setTaskToDelete({ id: taskId, name: taskName }); + setIsDeleteTaskOpen(true); + }; + + const confirmDeleteTask = () => { + if (!taskToDelete) return; + + deleteTask({ + variables: { + connections: [`${data.control?.tasks?.__id}`], + input: { + taskId: taskToDelete.id, + }, + }, + onCompleted: () => { + toast({ + title: "Task deleted", + description: "Task has been deleted successfully.", + }); + setIsDeleteTaskOpen(false); + setTaskToDelete(null); + }, + onError: (error) => { + toast({ + title: "Error deleting task", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + return (
@@ -339,16 +394,16 @@ function ControlOverviewPageContent({
06.00 - 07.30
-
@@ -362,6 +417,30 @@ function ControlOverviewPageContent({ )} + + {/* Delete Task Confirmation Dialog */} + + + + Delete Task + + Are you sure you want to delete the task " + {taskToDelete?.name}"? This action cannot be undone. + + + + + + + + ); } diff --git a/apps/console/src/pages/__generated__/ControlOverviewPageDeleteTaskMutation.graphql.ts b/apps/console/src/pages/__generated__/ControlOverviewPageDeleteTaskMutation.graphql.ts new file mode 100644 index 000000000..9fcc17e52 --- /dev/null +++ b/apps/console/src/pages/__generated__/ControlOverviewPageDeleteTaskMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteTaskInput = { + taskId: string; +}; +export type ControlOverviewPageDeleteTaskMutation$variables = { + connections: ReadonlyArray; + input: DeleteTaskInput; +}; +export type ControlOverviewPageDeleteTaskMutation$data = { + readonly deleteTask: { + readonly deletedTaskId: string; + }; +}; +export type ControlOverviewPageDeleteTaskMutation = { + response: ControlOverviewPageDeleteTaskMutation$data; + variables: ControlOverviewPageDeleteTaskMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedTaskId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "ControlOverviewPageDeleteTaskMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteTaskPayload", + "kind": "LinkedField", + "name": "deleteTask", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "ControlOverviewPageDeleteTaskMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteTaskPayload", + "kind": "LinkedField", + "name": "deleteTask", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedTaskId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "06db9599a3e83354c0e865d08cdb29dd", + "id": null, + "metadata": {}, + "name": "ControlOverviewPageDeleteTaskMutation", + "operationKind": "mutation", + "text": "mutation ControlOverviewPageDeleteTaskMutation(\n $input: DeleteTaskInput!\n) {\n deleteTask(input: $input) {\n deletedTaskId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "885a98007e48ab3175d0480c5a750e19"; + +export default node; diff --git a/pkg/api/console/v1/schema.graphql b/pkg/api/console/v1/schema.graphql index f620c4e7c..a7c2a32f8 100644 --- a/pkg/api/console/v1/schema.graphql +++ b/pkg/api/console/v1/schema.graphql @@ -393,6 +393,7 @@ type Mutation { ): DeleteOrganizationPayload! updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload! createTask(input: CreateTaskInput!): CreateTaskPayload! + deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! } input CreateVendorInput { @@ -531,3 +532,11 @@ input CreateTaskInput { type CreateTaskPayload { taskEdge: TaskEdge! } + +input DeleteTaskInput { + taskId: ID! +} + +type DeleteTaskPayload { + deletedTaskId: ID! +} diff --git a/pkg/api/console/v1/schema/schema.go b/pkg/api/console/v1/schema/schema.go index a176b9885..a4b03a600 100644 --- a/pkg/api/console/v1/schema/schema.go +++ b/pkg/api/console/v1/schema/schema.go @@ -121,6 +121,10 @@ type ComplexityRoot struct { DeletedPeopleID func(childComplexity int) int } + DeleteTaskPayload struct { + DeletedTaskID func(childComplexity int) int + } + DeleteVendorPayload struct { DeletedVendorID func(childComplexity int) int } @@ -191,6 +195,7 @@ type ComplexityRoot struct { CreateVendor func(childComplexity int, input types.CreateVendorInput) int DeleteOrganization func(childComplexity int, input types.DeleteOrganizationInput) int DeletePeople func(childComplexity int, input types.DeletePeopleInput) int + DeleteTask func(childComplexity int, input types.DeleteTaskInput) int DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int UpdateTaskState func(childComplexity int, input types.UpdateTaskStateInput) int @@ -357,6 +362,7 @@ type MutationResolver interface { DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) UpdateTaskState(ctx context.Context, input types.UpdateTaskStateInput) (*types.UpdateTaskStatePayload, error) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) + DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error) } type OrganizationResolver interface { Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) @@ -607,6 +613,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DeletePeoplePayload.DeletedPeopleID(childComplexity), true + case "DeleteTaskPayload.deletedTaskId": + if e.complexity.DeleteTaskPayload.DeletedTaskID == nil { + break + } + + return e.complexity.DeleteTaskPayload.DeletedTaskID(childComplexity), true + case "DeleteVendorPayload.deletedVendorId": if e.complexity.DeleteVendorPayload.DeletedVendorID == nil { break @@ -920,6 +933,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeletePeople(childComplexity, args["input"].(types.DeletePeopleInput)), true + case "Mutation.deleteTask": + if e.complexity.Mutation.DeleteTask == nil { + break + } + + args, err := ec.field_Mutation_deleteTask_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteTask(childComplexity, args["input"].(types.DeleteTaskInput)), true + case "Mutation.deleteVendor": if e.complexity.Mutation.DeleteVendor == nil { break @@ -1563,6 +1588,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputCreateVendorInput, ec.unmarshalInputDeleteOrganizationInput, ec.unmarshalInputDeletePeopleInput, + ec.unmarshalInputDeleteTaskInput, ec.unmarshalInputDeleteVendorInput, ec.unmarshalInputUpdatePeopleInput, ec.unmarshalInputUpdateTaskStateInput, @@ -2059,6 +2085,7 @@ type Mutation { ): DeleteOrganizationPayload! updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload! createTask(input: CreateTaskInput!): CreateTaskPayload! + deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! } input CreateVendorInput { @@ -2197,6 +2224,14 @@ input CreateTaskInput { type CreateTaskPayload { taskEdge: TaskEdge! } + +input DeleteTaskInput { + taskId: ID! +} + +type DeleteTaskPayload { + deletedTaskId: ID! +} `, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -2651,6 +2686,29 @@ func (ec *executionContext) field_Mutation_deletePeople_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_deleteTask_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_deleteTask_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_deleteTask_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.DeleteTaskInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNDeleteTaskInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteTaskInput(ctx, tmp) + } + + var zeroVal types.DeleteTaskInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_deleteVendor_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -4513,6 +4571,44 @@ func (ec *executionContext) fieldContext_DeletePeoplePayload_deletedPeopleId(_ c return fc, nil } +func (ec *executionContext) _DeleteTaskPayload_deletedTaskId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteTaskPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DeleteTaskPayload_deletedTaskId(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.DeletedTaskID, 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.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DeleteTaskPayload_deletedTaskId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DeleteTaskPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _DeleteVendorPayload_deletedVendorId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteVendorPayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DeleteVendorPayload_deletedVendorId(ctx, field) if err != nil { @@ -6385,6 +6481,53 @@ func (ec *executionContext) fieldContext_Mutation_createTask(ctx context.Context return fc, nil } +func (ec *executionContext) _Mutation_deleteTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteTask(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 ec.resolvers.Mutation().DeleteTask(rctx, fc.Args["input"].(types.DeleteTaskInput)) + }) + 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.(*types.DeleteTaskPayload) + fc.Result = res + return ec.marshalNDeleteTaskPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteTaskPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteTask(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "deletedTaskId": + return ec.fieldContext_DeleteTaskPayload_deletedTaskId(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DeleteTaskPayload", field.Name) + }, + } + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteTask_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Organization_id(ctx, field) if err != nil { @@ -11533,6 +11676,33 @@ func (ec *executionContext) unmarshalInputDeletePeopleInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputDeleteTaskInput(ctx context.Context, obj any) (types.DeleteTaskInput, error) { + var it types.DeleteTaskInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"taskId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "taskId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("taskId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.TaskID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteVendorInput(ctx context.Context, obj any) (types.DeleteVendorInput, error) { var it types.DeleteVendorInput asMap := map[string]any{} @@ -12425,6 +12595,45 @@ func (ec *executionContext) _DeletePeoplePayload(ctx context.Context, sel ast.Se return out } +var deleteTaskPayloadImplementors = []string{"DeleteTaskPayload"} + +func (ec *executionContext) _DeleteTaskPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteTaskPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, deleteTaskPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DeleteTaskPayload") + case "deletedTaskId": + out.Values[i] = ec._DeleteTaskPayload_deletedTaskId(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var deleteVendorPayloadImplementors = []string{"DeleteVendorPayload"} func (ec *executionContext) _DeleteVendorPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteVendorPayload) graphql.Marshaler { @@ -13065,6 +13274,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "deleteTask": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteTask(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -14990,6 +15206,25 @@ func (ec *executionContext) marshalNDeletePeoplePayload2ᚖgithubᚗcomᚋgetpro return ec._DeletePeoplePayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNDeleteTaskInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteTaskInput(ctx context.Context, v any) (types.DeleteTaskInput, error) { + res, err := ec.unmarshalInputDeleteTaskInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDeleteTaskPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteTaskPayload(ctx context.Context, sel ast.SelectionSet, v types.DeleteTaskPayload) graphql.Marshaler { + return ec._DeleteTaskPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDeleteTaskPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteTaskPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteTaskPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DeleteTaskPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNDeleteVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteVendorInput(ctx context.Context, v any) (types.DeleteVendorInput, error) { res, err := ec.unmarshalInputDeleteVendorInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/api/console/v1/types/types.go b/pkg/api/console/v1/types/types.go index bfc6f1e89..8f28c5f43 100644 --- a/pkg/api/console/v1/types/types.go +++ b/pkg/api/console/v1/types/types.go @@ -122,6 +122,14 @@ type DeletePeoplePayload struct { DeletedPeopleID gid.GID `json:"deletedPeopleId"` } +type DeleteTaskInput struct { + TaskID gid.GID `json:"taskId"` +} + +type DeleteTaskPayload struct { + DeletedTaskID gid.GID `json:"deletedTaskId"` +} + type DeleteVendorInput struct { VendorID gid.GID `json:"vendorId"` } diff --git a/pkg/api/console/v1/v1_resolver.go b/pkg/api/console/v1/v1_resolver.go index 5964ad016..0f96e9f2e 100644 --- a/pkg/api/console/v1/v1_resolver.go +++ b/pkg/api/console/v1/v1_resolver.go @@ -225,6 +225,18 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTas }, nil } +// DeleteTask is the resolver for the deleteTask field. +func (r *mutationResolver) DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error) { + err := r.proboSvc.DeleteTask(ctx, input.TaskID) + if err != nil { + return nil, fmt.Errorf("cannot delete task: %w", err) + } + + return &types.DeleteTaskPayload{ + DeletedTaskID: input.TaskID, + }, nil +} + // Frameworks is the resolver for the frameworks field. func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) { cursor := types.NewCursor(first, after, last, before) diff --git a/pkg/probo/coredata/task.go b/pkg/probo/coredata/task.go index 1903521f5..bf03897b5 100644 --- a/pkg/probo/coredata/task.go +++ b/pkg/probo/coredata/task.go @@ -269,3 +269,75 @@ WHERE return nil } + +func (t *Task) Delete( + ctx context.Context, + conn pg.Conn, + scope *Scope, +) error { + // Use a single transaction with conditional logic to handle both cases + q := ` +WITH control_count AS ( + SELECT COUNT(*) AS count FROM controls_tasks WHERE task_id = @task_id +), +delete_link AS ( + DELETE FROM controls_tasks + WHERE task_id = @task_id AND control_id = @control_id + RETURNING task_id +), +delete_transitions AS ( + DELETE FROM task_state_transitions + WHERE %s AND task_id = @task_id AND (SELECT count FROM control_count) <= 1 + RETURNING task_id +), +delete_all_links AS ( + DELETE FROM controls_tasks + WHERE task_id = @task_id AND (SELECT count FROM control_count) <= 1 + RETURNING task_id +), +delete_task AS ( + DELETE FROM tasks + WHERE %s AND id = @task_id AND (SELECT count FROM control_count) <= 1 + RETURNING id +) +SELECT + (SELECT count FROM control_count) AS control_count, + (SELECT COUNT(*) FROM delete_link) AS deleted_links, + (SELECT COUNT(*) FROM delete_transitions) AS deleted_transitions, + (SELECT COUNT(*) FROM delete_all_links) AS deleted_all_links, + (SELECT COUNT(*) FROM delete_task) AS deleted_tasks; +` + q = fmt.Sprintf(q, scope.SQLFragment(), scope.SQLFragment()) + + args := pgx.NamedArgs{ + "task_id": t.ID, + "control_id": t.ControlID, + } + maps.Copy(args, scope.SQLArguments()) + + var controlCount, deletedLinks, deletedTransitions, deletedAllLinks, deletedTasks int + err := conn.QueryRow(ctx, q, args).Scan( + &controlCount, + &deletedLinks, + &deletedTransitions, + &deletedAllLinks, + &deletedTasks, + ) + + if err != nil { + return fmt.Errorf("cannot execute delete operation: %w", err) + } + + if controlCount <= 1 { + if deletedTransitions == 0 || deletedAllLinks == 0 || deletedTasks == 0 { + return fmt.Errorf("failed to delete task completely: transitions=%d, links=%d, tasks=%d", + deletedTransitions, deletedAllLinks, deletedTasks) + } + } else { + if deletedLinks == 0 { + return fmt.Errorf("failed to delete control-task link") + } + } + + return nil +} diff --git a/pkg/probo/delete_task.go b/pkg/probo/delete_task.go new file mode 100644 index 000000000..5f6835658 --- /dev/null +++ b/pkg/probo/delete_task.go @@ -0,0 +1,22 @@ +package probo + +import ( + "context" + + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/probo/coredata" + "go.gearno.de/kit/pg" +) + +func (s Service) DeleteTask( + ctx context.Context, + taskID gid.GID, +) error { + task := coredata.Task{ID: taskID} + return s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return task.Delete(ctx, conn, s.scope) + }, + ) +}