Add update task state

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-26 15:25:58 +01:00
parent 0882a65e8d
commit 58fddfc240
9 changed files with 576 additions and 6 deletions

View File

@@ -5,12 +5,15 @@ import {
PreloadedQuery, PreloadedQuery,
usePreloadedQuery, usePreloadedQuery,
useQueryLoader, useQueryLoader,
useMutation,
} from "react-relay"; } from "react-relay";
import { CheckCircle2 } from "lucide-react"; import { CheckCircle2 } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql"; import type { ControlOverviewPageQuery as ControlOverviewPageQueryType } from "./__generated__/ControlOverviewPageQuery.graphql";
import type { ControlOverviewPageUpdateTaskStateMutation as ControlOverviewPageUpdateTaskStateMutationType } from "./__generated__/ControlOverviewPageUpdateTaskStateMutation.graphql";
const controlOverviewPageQuery = graphql` const controlOverviewPageQuery = graphql`
query ControlOverviewPageQuery($controlId: ID!) { query ControlOverviewPageQuery($controlId: ID!) {
@@ -36,6 +39,21 @@ const controlOverviewPageQuery = graphql`
} }
`; `;
const updateTaskStateMutation = graphql`
mutation ControlOverviewPageUpdateTaskStateMutation(
$input: UpdateTaskStateInput!
) {
updateTaskState(input: $input) {
taskEdge {
node {
id
state
}
}
}
}
`;
function ControlOverviewPageContent({ function ControlOverviewPageContent({
queryRef, queryRef,
}: { }: {
@@ -43,11 +61,52 @@ function ControlOverviewPageContent({
}) { }) {
const data = usePreloadedQuery<ControlOverviewPageQueryType>( const data = usePreloadedQuery<ControlOverviewPageQueryType>(
controlOverviewPageQuery, controlOverviewPageQuery,
queryRef, queryRef
); );
const { toast } = useToast();
const [updateTaskState] =
useMutation<ControlOverviewPageUpdateTaskStateMutationType>(
updateTaskStateMutation
);
const control = data.control; const control = data.control;
const tasks = control?.tasks?.edges.map((edge) => edge?.node) ?? []; const tasks = control?.tasks?.edges.map((edge) => edge?.node) ?? [];
const handleTaskClick = (taskId: string, currentState: string) => {
const newState = currentState === "DONE" ? "TODO" : "DONE";
updateTaskState({
variables: {
input: {
taskId,
state: newState,
},
},
optimisticResponse: {
updateTaskState: {
task: {
id: taskId,
state: newState,
},
},
},
onCompleted: () => {
toast({
title: "Task updated",
description: `Task has been ${
newState === "DONE" ? "completed" : "reopened"
}.`,
});
},
onError: (error) => {
toast({
title: "Error updating task",
description: error.message,
variant: "destructive",
});
},
});
};
return ( return (
<div className="min-h-screen bg-white p-6 space-y-6"> <div className="min-h-screen bg-white p-6 space-y-6">
<div className="space-y-4"> <div className="space-y-4">
@@ -101,12 +160,24 @@ function ControlOverviewPageContent({
? "border-gray-400 bg-gray-100" ? "border-gray-400 bg-gray-100"
: "border-gray-300" : "border-gray-300"
}`} }`}
onClick={() =>
task?.id &&
task?.state &&
handleTaskClick(task.id, task.state)
}
> >
{task?.state === "DONE" && ( {task?.state === "DONE" && (
<CheckCircle2 className="w-4 h-4 text-gray-500" /> <CheckCircle2 className="w-4 h-4 text-gray-500" />
)} )}
</div> </div>
<div className="flex-1 flex items-center justify-between"> <div
className="flex-1 flex items-center justify-between cursor-pointer"
onClick={() =>
task?.id &&
task?.state &&
handleTaskClick(task.id, task.state)
}
>
<div> <div>
<h3 <h3
className={`text-sm ${ className={`text-sm ${
@@ -165,7 +236,7 @@ function ControlOverviewPageFallback() {
export default function ControlOverviewPage() { export default function ControlOverviewPage() {
const { controlId } = useParams(); const { controlId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<ControlOverviewPageQueryType>( const [queryRef, loadQuery] = useQueryLoader<ControlOverviewPageQueryType>(
controlOverviewPageQuery, controlOverviewPageQuery
); );
useEffect(() => { useEffect(() => {

View File

@@ -0,0 +1,128 @@
/**
* @generated SignedSource<<191b5c89c10a7d949997216a79f49598>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type TaskState = "DONE" | "TODO";
export type UpdateTaskStateInput = {
state: TaskState;
taskId: string;
};
export type ControlOverviewPageUpdateTaskStateMutation$variables = {
input: UpdateTaskStateInput;
};
export type ControlOverviewPageUpdateTaskStateMutation$data = {
readonly updateTaskState: {
readonly taskEdge: {
readonly node: {
readonly id: string;
readonly state: TaskState;
};
};
};
};
export type ControlOverviewPageUpdateTaskStateMutation = {
response: ControlOverviewPageUpdateTaskStateMutation$data;
variables: ControlOverviewPageUpdateTaskStateMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateTaskStatePayload",
"kind": "LinkedField",
"name": "updateTaskState",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TaskEdge",
"kind": "LinkedField",
"name": "taskEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Task",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ControlOverviewPageUpdateTaskStateMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ControlOverviewPageUpdateTaskStateMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "ea56b9c3f41a4b69a3643884f8f2306d",
"id": null,
"metadata": {},
"name": "ControlOverviewPageUpdateTaskStateMutation",
"operationKind": "mutation",
"text": "mutation ControlOverviewPageUpdateTaskStateMutation(\n $input: UpdateTaskStateInput!\n) {\n updateTaskState(input: $input) {\n taskEdge {\n node {\n id\n state\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "3113753b83cf06bd8aca41610c1e89fd";
export default node;

View File

@@ -391,6 +391,7 @@ type Mutation {
deleteOrganization( deleteOrganization(
input: DeleteOrganizationInput! input: DeleteOrganizationInput!
): DeleteOrganizationPayload! ): DeleteOrganizationPayload!
updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload!
} }
input CreateVendorInput { input CreateVendorInput {
@@ -510,3 +511,12 @@ type CreateOrganizationPayload {
type DeleteOrganizationPayload { type DeleteOrganizationPayload {
deletedOrganizationId: ID! deletedOrganizationId: ID!
} }
input UpdateTaskStateInput {
taskId: ID!
state: TaskState!
}
type UpdateTaskStatePayload {
taskEdge: TaskEdge!
}

View File

@@ -188,6 +188,7 @@ type ComplexityRoot struct {
DeletePeople func(childComplexity int, input types.DeletePeopleInput) int DeletePeople func(childComplexity int, input types.DeletePeopleInput) int
DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int
UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int
UpdateTaskState func(childComplexity int, input types.UpdateTaskStateInput) int
UpdateVendor func(childComplexity int, input types.UpdateVendorInput) int UpdateVendor func(childComplexity int, input types.UpdateVendorInput) int
} }
@@ -290,6 +291,10 @@ type ComplexityRoot struct {
Node func(childComplexity int) int Node func(childComplexity int) int
} }
UpdateTaskStatePayload struct {
TaskEdge func(childComplexity int) int
}
User struct { User struct {
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Email func(childComplexity int) int Email func(childComplexity int) int
@@ -345,6 +350,7 @@ type MutationResolver interface {
DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error)
CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error)
DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error)
UpdateTaskState(ctx context.Context, input types.UpdateTaskStateInput) (*types.UpdateTaskStatePayload, error)
} }
type OrganizationResolver interface { type OrganizationResolver interface {
Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error)
@@ -913,6 +919,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdatePeople(childComplexity, args["input"].(types.UpdatePeopleInput)), true return e.complexity.Mutation.UpdatePeople(childComplexity, args["input"].(types.UpdatePeopleInput)), true
case "Mutation.updateTaskState":
if e.complexity.Mutation.UpdateTaskState == nil {
break
}
args, err := ec.field_Mutation_updateTaskState_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateTaskState(childComplexity, args["input"].(types.UpdateTaskStateInput)), true
case "Mutation.updateVendor": case "Mutation.updateVendor":
if e.complexity.Mutation.UpdateVendor == nil { if e.complexity.Mutation.UpdateVendor == nil {
break break
@@ -1333,6 +1351,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TaskStateTransitionEdge.Node(childComplexity), true return e.complexity.TaskStateTransitionEdge.Node(childComplexity), true
case "UpdateTaskStatePayload.taskEdge":
if e.complexity.UpdateTaskStatePayload.TaskEdge == nil {
break
}
return e.complexity.UpdateTaskStatePayload.TaskEdge(childComplexity), true
case "User.createdAt": case "User.createdAt":
if e.complexity.User.CreatedAt == nil { if e.complexity.User.CreatedAt == nil {
break break
@@ -1514,6 +1539,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputDeletePeopleInput, ec.unmarshalInputDeletePeopleInput,
ec.unmarshalInputDeleteVendorInput, ec.unmarshalInputDeleteVendorInput,
ec.unmarshalInputUpdatePeopleInput, ec.unmarshalInputUpdatePeopleInput,
ec.unmarshalInputUpdateTaskStateInput,
ec.unmarshalInputUpdateVendorInput, ec.unmarshalInputUpdateVendorInput,
) )
first := true first := true
@@ -2005,6 +2031,7 @@ type Mutation {
deleteOrganization( deleteOrganization(
input: DeleteOrganizationInput! input: DeleteOrganizationInput!
): DeleteOrganizationPayload! ): DeleteOrganizationPayload!
updateTaskState(input: UpdateTaskStateInput!): UpdateTaskStatePayload!
} }
input CreateVendorInput { input CreateVendorInput {
@@ -2124,6 +2151,15 @@ type CreateOrganizationPayload {
type DeleteOrganizationPayload { type DeleteOrganizationPayload {
deletedOrganizationId: ID! deletedOrganizationId: ID!
} }
input UpdateTaskStateInput {
taskId: ID!
state: TaskState!
}
type UpdateTaskStatePayload {
taskEdge: TaskEdge!
}
`, BuiltIn: false}, `, BuiltIn: false},
} }
var parsedSchema = gqlparser.MustLoadSchema(sources...) var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -2601,6 +2637,29 @@ func (ec *executionContext) field_Mutation_updatePeople_argsInput(
return zeroVal, nil return zeroVal, nil
} }
func (ec *executionContext) field_Mutation_updateTaskState_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_updateTaskState_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_updateTaskState_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.UpdateTaskStateInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNUpdateTaskStateInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateTaskStateInput(ctx, tmp)
}
var zeroVal types.UpdateTaskStateInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_updateVendor_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Mutation_updateVendor_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -6128,6 +6187,53 @@ func (ec *executionContext) fieldContext_Mutation_deleteOrganization(ctx context
return fc, nil return fc, nil
} }
func (ec *executionContext) _Mutation_updateTaskState(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_updateTaskState(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().UpdateTaskState(rctx, fc.Args["input"].(types.UpdateTaskStateInput))
})
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.UpdateTaskStatePayload)
fc.Result = res
return ec.marshalNUpdateTaskStatePayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateTaskStatePayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_updateTaskState(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 "taskEdge":
return ec.fieldContext_UpdateTaskStatePayload_taskEdge(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type UpdateTaskStatePayload", field.Name)
},
}
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_updateTaskState_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) { 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) fc, err := ec.fieldContext_Organization_id(ctx, field)
if err != nil { if err != nil {
@@ -8485,6 +8591,50 @@ func (ec *executionContext) fieldContext_TaskStateTransitionEdge_node(_ context.
return fc, nil return fc, nil
} }
func (ec *executionContext) _UpdateTaskStatePayload_taskEdge(ctx context.Context, field graphql.CollectedField, obj *types.UpdateTaskStatePayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UpdateTaskStatePayload_taskEdge(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.TaskEdge, 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.(*types.TaskEdge)
fc.Result = res
return ec.marshalNTaskEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐTaskEdge(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UpdateTaskStatePayload_taskEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UpdateTaskStatePayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "cursor":
return ec.fieldContext_TaskEdge_cursor(ctx, field)
case "node":
return ec.fieldContext_TaskEdge_node(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type TaskEdge", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) { func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_id(ctx, field) fc, err := ec.fieldContext_User_id(ctx, field)
if err != nil { if err != nil {
@@ -11268,6 +11418,40 @@ func (ec *executionContext) unmarshalInputUpdatePeopleInput(ctx context.Context,
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputUpdateTaskStateInput(ctx context.Context, obj any) (types.UpdateTaskStateInput, error) {
var it types.UpdateTaskStateInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"taskId", "state"}
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
case "state":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
data, err := ec.unmarshalNTaskState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐTaskState(ctx, v)
if err != nil {
return it, err
}
it.State = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateVendorInput(ctx context.Context, obj any) (types.UpdateVendorInput, error) { func (ec *executionContext) unmarshalInputUpdateVendorInput(ctx context.Context, obj any) (types.UpdateVendorInput, error) {
var it types.UpdateVendorInput var it types.UpdateVendorInput
asMap := map[string]any{} asMap := map[string]any{}
@@ -12624,6 +12808,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "updateTaskState":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_updateTaskState(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -13585,6 +13776,45 @@ func (ec *executionContext) _TaskStateTransitionEdge(ctx context.Context, sel as
return out return out
} }
var updateTaskStatePayloadImplementors = []string{"UpdateTaskStatePayload"}
func (ec *executionContext) _UpdateTaskStatePayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateTaskStatePayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, updateTaskStatePayloadImplementors)
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("UpdateTaskStatePayload")
case "taskEdge":
out.Values[i] = ec._UpdateTaskStatePayload_taskEdge(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 userImplementors = []string{"User", "Node"} var userImplementors = []string{"User", "Node"}
func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *types.User) graphql.Marshaler { func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *types.User) graphql.Marshaler {
@@ -15261,6 +15491,25 @@ func (ec *executionContext) unmarshalNUpdatePeopleInput2githubᚗcomᚋgetprobo
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
} }
func (ec *executionContext) unmarshalNUpdateTaskStateInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateTaskStateInput(ctx context.Context, v any) (types.UpdateTaskStateInput, error) {
res, err := ec.unmarshalInputUpdateTaskStateInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNUpdateTaskStatePayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateTaskStatePayload(ctx context.Context, sel ast.SelectionSet, v types.UpdateTaskStatePayload) graphql.Marshaler {
return ec._UpdateTaskStatePayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNUpdateTaskStatePayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateTaskStatePayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateTaskStatePayload) 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._UpdateTaskStatePayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNUpdateVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateVendorInput(ctx context.Context, v any) (types.UpdateVendorInput, error) { func (ec *executionContext) unmarshalNUpdateVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateVendorInput(ctx context.Context, v any) (types.UpdateVendorInput, error) {
res, err := ec.unmarshalInputUpdateVendorInput(ctx, v) res, err := ec.unmarshalInputUpdateVendorInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -303,6 +303,15 @@ type UpdatePeopleInput struct {
Kind *coredata.PeopleKind `json:"kind,omitempty"` Kind *coredata.PeopleKind `json:"kind,omitempty"`
} }
type UpdateTaskStateInput struct {
TaskID gid.GID `json:"taskId"`
State coredata.TaskState `json:"state"`
}
type UpdateTaskStatePayload struct {
TaskEdge *TaskEdge `json:"taskEdge"`
}
type UpdateVendorInput struct { type UpdateVendorInput struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"` ExpectedVersion int `json:"expectedVersion"`

View File

@@ -193,6 +193,22 @@ func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.D
panic(fmt.Errorf("not implemented: DeleteOrganization - deleteOrganization")) panic(fmt.Errorf("not implemented: DeleteOrganization - deleteOrganization"))
} }
// UpdateTaskState is the resolver for the updateTaskState field.
func (r *mutationResolver) UpdateTaskState(ctx context.Context, input types.UpdateTaskStateInput) (*types.UpdateTaskStatePayload, error) {
task, err := r.proboSvc.UpdateTaskState(ctx, probo.UpdateTaskStateRequest{
TaskID: input.TaskID,
State: input.State,
Reason: nil,
})
if err != nil {
return nil, fmt.Errorf("cannot update task state: %w", err)
}
return &types.UpdateTaskStatePayload{
TaskEdge: types.NewTaskEdge(task),
}, nil
}
// Frameworks is the resolver for the frameworks field. // 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) { 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) cursor := types.NewCursor(first, after, last, before)

View File

@@ -59,8 +59,8 @@ func (tst TaskStateTransition) Insert(
INSERT INTO INSERT INTO
task_state_transitions ( task_state_transitions (
id, id,
control_id, task_id,
from_state from_state,
to_state, to_state,
reason, reason,
created_at, created_at,

View File

@@ -0,0 +1,88 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package probo
import (
"context"
"fmt"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/probo/coredata"
"go.gearno.de/kit/pg"
)
type UpdateTaskStateRequest struct {
TaskID gid.GID
State coredata.TaskState
Reason *string
}
func (s Service) UpdateTaskState(
ctx context.Context,
req UpdateTaskStateRequest,
) (*coredata.Task, error) {
// TODO: lock the task for update to ensure that only one update can happen at a time
task, err := s.GetTask(ctx, req.TaskID)
if err != nil {
return nil, fmt.Errorf("cannot get task: %w", err)
}
if task.State == req.State {
return task, nil
}
taskStateTransitionID, err := gid.NewGID(coredata.TaskStateTransitionEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create task state transition global id: %w", err)
}
now := time.Now()
currentState := task.State
taskStateTransition := coredata.TaskStateTransition{
StateTransition: coredata.StateTransition[coredata.TaskState]{
ID: taskStateTransitionID,
FromState: &currentState,
ToState: req.State,
Reason: req.Reason,
CreatedAt: now,
UpdatedAt: now,
},
TaskID: task.ID,
}
task.State = req.State
task.UpdatedAt = now
err = s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := taskStateTransition.Insert(ctx, conn); err != nil {
return fmt.Errorf("cannot insert task state transition: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return task, nil
}

View File

@@ -106,7 +106,6 @@ func (impl *Implm) Run(
return fmt.Errorf("cannot create pg client: %w", err) return fmt.Errorf("cannot create pg client: %w", err)
} }
// Get the pepper bytes for password hashing
pepper, err := impl.cfg.Auth.GetPepperBytes() pepper, err := impl.cfg.Auth.GetPepperBytes()
if err != nil { if err != nil {
return fmt.Errorf("cannot get pepper bytes: %w", err) return fmt.Errorf("cannot get pepper bytes: %w", err)