feature: ability to delete task groups

This commit is contained in:
Jordan Knott 2020-04-11 14:24:45 -05:00
parent 063be79b89
commit c250ce574b
27 changed files with 824 additions and 221 deletions

View File

@ -50,6 +50,12 @@ type DirectiveRoot struct {
} }
type ComplexityRoot struct { type ComplexityRoot struct {
DeleteTaskGroupPayload struct {
AffectedRows func(childComplexity int) int
Ok func(childComplexity int) int
TaskGroup func(childComplexity int) int
}
DeleteTaskPayload struct { DeleteTaskPayload struct {
TaskID func(childComplexity int) int TaskID func(childComplexity int) int
} }
@ -63,6 +69,7 @@ type ComplexityRoot struct {
CreateTeam func(childComplexity int, input NewTeam) int CreateTeam func(childComplexity int, input NewTeam) int
CreateUserAccount func(childComplexity int, input NewUserAccount) int CreateUserAccount func(childComplexity int, input NewUserAccount) int
DeleteTask func(childComplexity int, input DeleteTaskInput) int DeleteTask func(childComplexity int, input DeleteTaskInput) int
DeleteTaskGroup func(childComplexity int, input DeleteTaskGroupInput) int
LogoutUser func(childComplexity int, input LogoutUser) int LogoutUser func(childComplexity int, input LogoutUser) int
UpdateTaskGroupLocation func(childComplexity int, input NewTaskGroupLocation) int UpdateTaskGroupLocation func(childComplexity int, input NewTaskGroupLocation) int
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
@ -142,11 +149,12 @@ type MutationResolver interface {
CreateProject(ctx context.Context, input NewProject) (*pg.Project, error) CreateProject(ctx context.Context, input NewProject) (*pg.Project, error)
CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error)
UpdateTaskGroupLocation(ctx context.Context, input NewTaskGroupLocation) (*pg.TaskGroup, error) UpdateTaskGroupLocation(ctx context.Context, input NewTaskGroupLocation) (*pg.TaskGroup, error)
DeleteTaskGroup(ctx context.Context, input DeleteTaskGroupInput) (*DeleteTaskGroupPayload, error)
CreateTask(ctx context.Context, input NewTask) (*pg.Task, error) CreateTask(ctx context.Context, input NewTask) (*pg.Task, error)
UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*pg.Task, error) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*pg.Task, error)
LogoutUser(ctx context.Context, input LogoutUser) (bool, error)
UpdateTaskName(ctx context.Context, input UpdateTaskName) (*pg.Task, error) UpdateTaskName(ctx context.Context, input UpdateTaskName) (*pg.Task, error)
DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error) DeleteTask(ctx context.Context, input DeleteTaskInput) (*DeleteTaskPayload, error)
LogoutUser(ctx context.Context, input LogoutUser) (bool, error)
} }
type OrganizationResolver interface { type OrganizationResolver interface {
Teams(ctx context.Context, obj *pg.Organization) ([]pg.Team, error) Teams(ctx context.Context, obj *pg.Organization) ([]pg.Team, error)
@ -192,6 +200,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
_ = ec _ = ec
switch typeName + "." + field { switch typeName + "." + field {
case "DeleteTaskGroupPayload.affectedRows":
if e.complexity.DeleteTaskGroupPayload.AffectedRows == nil {
break
}
return e.complexity.DeleteTaskGroupPayload.AffectedRows(childComplexity), true
case "DeleteTaskGroupPayload.ok":
if e.complexity.DeleteTaskGroupPayload.Ok == nil {
break
}
return e.complexity.DeleteTaskGroupPayload.Ok(childComplexity), true
case "DeleteTaskGroupPayload.taskGroup":
if e.complexity.DeleteTaskGroupPayload.TaskGroup == nil {
break
}
return e.complexity.DeleteTaskGroupPayload.TaskGroup(childComplexity), true
case "DeleteTaskPayload.taskID": case "DeleteTaskPayload.taskID":
if e.complexity.DeleteTaskPayload.TaskID == nil { if e.complexity.DeleteTaskPayload.TaskID == nil {
break break
@ -295,6 +324,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.DeleteTask(childComplexity, args["input"].(DeleteTaskInput)), true return e.complexity.Mutation.DeleteTask(childComplexity, args["input"].(DeleteTaskInput)), true
case "Mutation.deleteTaskGroup":
if e.complexity.Mutation.DeleteTaskGroup == nil {
break
}
args, err := ec.field_Mutation_deleteTaskGroup_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.DeleteTaskGroup(childComplexity, args["input"].(DeleteTaskGroupInput)), true
case "Mutation.logoutUser": case "Mutation.logoutUser":
if e.complexity.Mutation.LogoutUser == nil { if e.complexity.Mutation.LogoutUser == nil {
break break
@ -845,19 +886,37 @@ input NewTaskGroupLocation {
position: Float! position: Float!
} }
input DeleteTaskGroupInput {
taskGroupID: UUID!
}
type DeleteTaskGroupPayload {
ok: Boolean!
affectedRows: Int!
taskGroup: TaskGroup!
}
type Mutation { type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken! createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount! createUserAccount(input: NewUserAccount!): UserAccount!
createOrganization(input: NewOrganization!): Organization! createOrganization(input: NewOrganization!): Organization!
createTeam(input: NewTeam!): Team! createTeam(input: NewTeam!): Team!
createProject(input: NewProject!): Project! createProject(input: NewProject!): Project!
createTaskGroup(input: NewTaskGroup!): TaskGroup! createTaskGroup(input: NewTaskGroup!): TaskGroup!
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup! updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
createTask(input: NewTask!): Task! createTask(input: NewTask!): Task!
updateTaskLocation(input: NewTaskLocation!): Task! updateTaskLocation(input: NewTaskLocation!): Task!
logoutUser(input: LogoutUser!): Boolean!
updateTaskName(input: UpdateTaskName!): Task! updateTaskName(input: UpdateTaskName!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
logoutUser(input: LogoutUser!): Boolean!
} }
`, BuiltIn: false}, `, BuiltIn: false},
} }
@ -965,6 +1024,20 @@ func (ec *executionContext) field_Mutation_createUserAccount_args(ctx context.Co
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_deleteTaskGroup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 DeleteTaskGroupInput
if tmp, ok := rawArgs["input"]; ok {
arg0, err = ec.unmarshalNDeleteTaskGroupInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskGroupInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_deleteTask_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_deleteTask_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -1127,6 +1200,108 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg
// region **************************** field.gotpl ***************************** // region **************************** field.gotpl *****************************
func (ec *executionContext) _DeleteTaskGroupPayload_ok(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskGroupPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "DeleteTaskGroupPayload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Ok, 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.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _DeleteTaskGroupPayload_affectedRows(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskGroupPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "DeleteTaskGroupPayload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.AffectedRows, 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.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _DeleteTaskGroupPayload_taskGroup(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskGroupPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "DeleteTaskGroupPayload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.TaskGroup, 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.(*pg.TaskGroup)
fc.Result = res
return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTaskGroup(ctx, field.Selections, res)
}
func (ec *executionContext) _DeleteTaskPayload_taskID(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskPayload) (ret graphql.Marshaler) { func (ec *executionContext) _DeleteTaskPayload_taskID(ctx context.Context, field graphql.CollectedField, obj *DeleteTaskPayload) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -1448,6 +1623,47 @@ func (ec *executionContext) _Mutation_updateTaskGroupLocation(ctx context.Contex
return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTaskGroup(ctx, field.Selections, res) return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTaskGroup(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_deleteTaskGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_deleteTaskGroup_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().DeleteTaskGroup(rctx, args["input"].(DeleteTaskGroupInput))
})
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.(*DeleteTaskGroupPayload)
fc.Result = res
return ec.marshalNDeleteTaskGroupPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskGroupPayload(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_createTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_createTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -1530,47 +1746,6 @@ func (ec *executionContext) _Mutation_updateTaskLocation(ctx context.Context, fi
return ec.marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTask(ctx, field.Selections, res) return ec.marshalNTask2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋpgᚐTask(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_logoutUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_logoutUser_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().LogoutUser(rctx, args["input"].(LogoutUser))
})
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.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_updateTaskName(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_updateTaskName(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -1653,6 +1828,47 @@ func (ec *executionContext) _Mutation_deleteTask(ctx context.Context, field grap
return ec.marshalNDeleteTaskPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskPayload(ctx, field.Selections, res) return ec.marshalNDeleteTaskPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskPayload(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_logoutUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_logoutUser_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().LogoutUser(rctx, args["input"].(LogoutUser))
})
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.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Organization_organizationID(ctx context.Context, field graphql.CollectedField, obj *pg.Organization) (ret graphql.Marshaler) { func (ec *executionContext) _Organization_organizationID(ctx context.Context, field graphql.CollectedField, obj *pg.Organization) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -4158,6 +4374,24 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co
// region **************************** input.gotpl ***************************** // region **************************** input.gotpl *****************************
func (ec *executionContext) unmarshalInputDeleteTaskGroupInput(ctx context.Context, obj interface{}) (DeleteTaskGroupInput, error) {
var it DeleteTaskGroupInput
var asMap = obj.(map[string]interface{})
for k, v := range asMap {
switch k {
case "taskGroupID":
var err error
it.TaskGroupID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputDeleteTaskInput(ctx context.Context, obj interface{}) (DeleteTaskInput, error) { func (ec *executionContext) unmarshalInputDeleteTaskInput(ctx context.Context, obj interface{}) (DeleteTaskInput, error) {
var it DeleteTaskInput var it DeleteTaskInput
var asMap = obj.(map[string]interface{}) var asMap = obj.(map[string]interface{})
@ -4514,6 +4748,43 @@ func (ec *executionContext) unmarshalInputUpdateTaskName(ctx context.Context, ob
// region **************************** object.gotpl **************************** // region **************************** object.gotpl ****************************
var deleteTaskGroupPayloadImplementors = []string{"DeleteTaskGroupPayload"}
func (ec *executionContext) _DeleteTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, obj *DeleteTaskGroupPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, deleteTaskGroupPayloadImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("DeleteTaskGroupPayload")
case "ok":
out.Values[i] = ec._DeleteTaskGroupPayload_ok(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "affectedRows":
out.Values[i] = ec._DeleteTaskGroupPayload_affectedRows(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "taskGroup":
out.Values[i] = ec._DeleteTaskGroupPayload_taskGroup(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var deleteTaskPayloadImplementors = []string{"DeleteTaskPayload"} var deleteTaskPayloadImplementors = []string{"DeleteTaskPayload"}
func (ec *executionContext) _DeleteTaskPayload(ctx context.Context, sel ast.SelectionSet, obj *DeleteTaskPayload) graphql.Marshaler { func (ec *executionContext) _DeleteTaskPayload(ctx context.Context, sel ast.SelectionSet, obj *DeleteTaskPayload) graphql.Marshaler {
@ -4591,6 +4862,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "deleteTaskGroup":
out.Values[i] = ec._Mutation_deleteTaskGroup(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "createTask": case "createTask":
out.Values[i] = ec._Mutation_createTask(ctx, field) out.Values[i] = ec._Mutation_createTask(ctx, field)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@ -4601,11 +4877,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "logoutUser":
out.Values[i] = ec._Mutation_logoutUser(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "updateTaskName": case "updateTaskName":
out.Values[i] = ec._Mutation_updateTaskName(ctx, field) out.Values[i] = ec._Mutation_updateTaskName(ctx, field)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@ -4616,6 +4887,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "logoutUser":
out.Values[i] = ec._Mutation_logoutUser(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -5396,6 +5672,24 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
return res return res
} }
func (ec *executionContext) unmarshalNDeleteTaskGroupInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskGroupInput(ctx context.Context, v interface{}) (DeleteTaskGroupInput, error) {
return ec.unmarshalInputDeleteTaskGroupInput(ctx, v)
}
func (ec *executionContext) marshalNDeleteTaskGroupPayload2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, v DeleteTaskGroupPayload) graphql.Marshaler {
return ec._DeleteTaskGroupPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNDeleteTaskGroupPayload2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskGroupPayload(ctx context.Context, sel ast.SelectionSet, v *DeleteTaskGroupPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._DeleteTaskGroupPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNDeleteTaskInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskInput(ctx context.Context, v interface{}) (DeleteTaskInput, error) { func (ec *executionContext) unmarshalNDeleteTaskInput2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐDeleteTaskInput(ctx context.Context, v interface{}) (DeleteTaskInput, error) {
return ec.unmarshalInputDeleteTaskInput(ctx, v) return ec.unmarshalInputDeleteTaskInput(ctx, v)
} }
@ -5450,6 +5744,20 @@ func (ec *executionContext) marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx c
return res return res
} }
func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) {
return graphql.UnmarshalInt(v)
}
func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler {
res := graphql.MarshalInt(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
}
return res
}
func (ec *executionContext) unmarshalNLogoutUser2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐLogoutUser(ctx context.Context, v interface{}) (LogoutUser, error) { func (ec *executionContext) unmarshalNLogoutUser2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋgraphᚐLogoutUser(ctx context.Context, v interface{}) (LogoutUser, error) {
return ec.unmarshalInputLogoutUser(ctx, v) return ec.unmarshalInputLogoutUser(ctx, v)
} }

View File

@ -4,8 +4,19 @@ package graph
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/pg"
) )
type DeleteTaskGroupInput struct {
TaskGroupID uuid.UUID `json:"taskGroupID"`
}
type DeleteTaskGroupPayload struct {
Ok bool `json:"ok"`
AffectedRows int `json:"affectedRows"`
TaskGroup *pg.TaskGroup `json:"taskGroup"`
}
type DeleteTaskInput struct { type DeleteTaskInput struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
} }

View File

@ -141,17 +141,35 @@ input NewTaskGroupLocation {
position: Float! position: Float!
} }
input DeleteTaskGroupInput {
taskGroupID: UUID!
}
type DeleteTaskGroupPayload {
ok: Boolean!
affectedRows: Int!
taskGroup: TaskGroup!
}
type Mutation { type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken! createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount! createUserAccount(input: NewUserAccount!): UserAccount!
createOrganization(input: NewOrganization!): Organization! createOrganization(input: NewOrganization!): Organization!
createTeam(input: NewTeam!): Team! createTeam(input: NewTeam!): Team!
createProject(input: NewProject!): Project! createProject(input: NewProject!): Project!
createTaskGroup(input: NewTaskGroup!): TaskGroup! createTaskGroup(input: NewTaskGroup!): TaskGroup!
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup! updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
createTask(input: NewTask!): Task! createTask(input: NewTask!): Task!
updateTaskLocation(input: NewTaskLocation!): Task! updateTaskLocation(input: NewTaskLocation!): Task!
logoutUser(input: LogoutUser!): Boolean!
updateTaskName(input: UpdateTaskName!): Task! updateTaskName(input: UpdateTaskName!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload! deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
logoutUser(input: LogoutUser!): Boolean!
} }

View File

@ -73,6 +73,22 @@ func (r *mutationResolver) UpdateTaskGroupLocation(ctx context.Context, input Ne
return &taskGroup, err return &taskGroup, err
} }
func (r *mutationResolver) DeleteTaskGroup(ctx context.Context, input DeleteTaskGroupInput) (*DeleteTaskGroupPayload, error) {
deletedTasks, err := r.Repository.DeleteTasksByTaskGroupID(ctx, input.TaskGroupID)
if err != nil {
return &DeleteTaskGroupPayload{}, err
}
taskGroup, err := r.Repository.GetTaskGroupByID(ctx, input.TaskGroupID)
if err != nil {
return &DeleteTaskGroupPayload{}, err
}
deletedTaskGroups, err := r.Repository.DeleteTaskGroupByID(ctx, input.TaskGroupID)
if err != nil {
return &DeleteTaskGroupPayload{}, err
}
return &DeleteTaskGroupPayload{true, int(deletedTasks + deletedTaskGroups), &taskGroup}, nil
}
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.Task, error) { func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.Task, error) {
taskGroupID, err := uuid.Parse(input.TaskGroupID) taskGroupID, err := uuid.Parse(input.TaskGroupID)
createdAt := time.Now().UTC() createdAt := time.Now().UTC()
@ -98,16 +114,6 @@ func (r *mutationResolver) UpdateTaskLocation(ctx context.Context, input NewTask
return &task, err return &task, err
} }
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
userID, err := uuid.Parse(input.UserID)
if err != nil {
return false, err
}
err = r.Repository.DeleteRefreshTokenByUserID(ctx, userID)
return true, err
}
func (r *mutationResolver) UpdateTaskName(ctx context.Context, input UpdateTaskName) (*pg.Task, error) { func (r *mutationResolver) UpdateTaskName(ctx context.Context, input UpdateTaskName) (*pg.Task, error) {
taskID, err := uuid.Parse(input.TaskID) taskID, err := uuid.Parse(input.TaskID)
if err != nil { if err != nil {
@ -133,6 +139,16 @@ func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput
return &DeleteTaskPayload{taskID.String()}, nil return &DeleteTaskPayload{taskID.String()}, nil
} }
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
userID, err := uuid.Parse(input.UserID)
if err != nil {
return false, err
}
err = r.Repository.DeleteRefreshTokenByUserID(ctx, userID)
return true, err
}
func (r *organizationResolver) Teams(ctx context.Context, obj *pg.Organization) ([]pg.Team, error) { func (r *organizationResolver) Teams(ctx context.Context, obj *pg.Organization) ([]pg.Team, error) {
teams, err := r.Repository.GetTeamsForOrganization(ctx, obj.OrganizationID) teams, err := r.Repository.GetTeamsForOrganization(ctx, obj.OrganizationID)
return teams, err return teams, err

View File

@ -11,26 +11,35 @@ type Repository interface {
DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error) GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error)
GetAllTeams(ctx context.Context) ([]Team, error) GetAllTeams(ctx context.Context) ([]Team, error)
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
GetAllProjects(ctx context.Context) ([]Project, error) GetAllProjects(ctx context.Context) ([]Project, error)
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error) CreateUserAccount(ctx context.Context, arg CreateUserAccountParams) (UserAccount, error)
GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error)
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error) GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error
DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error
DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
UpdateTaskGroupLocation(ctx context.Context, arg UpdateTaskGroupLocationParams) (TaskGroup, error)
CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error) CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error)
GetAllTaskGroups(ctx context.Context) ([]TaskGroup, error) GetAllTaskGroups(ctx context.Context) ([]TaskGroup, error)
GetAllOrganizations(ctx context.Context) ([]Organization, error)
GetTaskGroupsForProject(ctx context.Context, projectID uuid.UUID) ([]TaskGroup, error) GetTaskGroupsForProject(ctx context.Context, projectID uuid.UUID) ([]TaskGroup, error)
GetTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (TaskGroup, error) GetTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (TaskGroup, error)
GetAllOrganizations(ctx context.Context) ([]Organization, error)
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error) GetTeamsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]Team, error)
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
GetAllTasks(ctx context.Context) ([]Task, error) GetAllTasks(ctx context.Context) ([]Task, error)
GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error) GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error)

View File

@ -20,6 +20,8 @@ type Querier interface {
DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error
DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error
DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error
DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
GetAllOrganizations(ctx context.Context) ([]Organization, error) GetAllOrganizations(ctx context.Context) ([]Organization, error)
GetAllProjects(ctx context.Context) ([]Project, error) GetAllProjects(ctx context.Context) ([]Project, error)

View File

@ -49,6 +49,18 @@ func (q *Queries) DeleteTaskByID(ctx context.Context, taskID uuid.UUID) error {
return err return err
} }
const deleteTasksByTaskGroupID = `-- name: DeleteTasksByTaskGroupID :execrows
DELETE FROM task where task_group_id = $1
`
func (q *Queries) DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteTasksByTaskGroupID, taskGroupID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getAllTasks = `-- name: GetAllTasks :many const getAllTasks = `-- name: GetAllTasks :many
SELECT task_id, task_group_id, created_at, name, position FROM task SELECT task_id, task_group_id, created_at, name, position FROM task
` `

View File

@ -40,6 +40,18 @@ func (q *Queries) CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams
return i, err return i, err
} }
const deleteTaskGroupByID = `-- name: DeleteTaskGroupByID :execrows
DELETE FROM task_group WHERE task_group_id = $1
`
func (q *Queries) DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteTaskGroupByID, taskGroupID)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
const getAllTaskGroups = `-- name: GetAllTaskGroups :many const getAllTaskGroups = `-- name: GetAllTaskGroups :many
SELECT task_group_id, project_id, created_at, name, position FROM task_group SELECT task_group_id, project_id, created_at, name, position FROM task_group
` `

View File

@ -16,3 +16,6 @@ DELETE FROM task WHERE task_id = $1;
-- name: UpdateTaskName :one -- name: UpdateTaskName :one
UPDATE task SET name = $2 WHERE task_id = $1 RETURNING *; UPDATE task SET name = $2 WHERE task_id = $1 RETURNING *;
-- name: DeleteTasksByTaskGroupID :execrows
DELETE FROM task where task_group_id = $1;

View File

@ -13,3 +13,6 @@ SELECT * FROM task_group WHERE task_group_id = $1;
-- name: UpdateTaskGroupLocation :one -- name: UpdateTaskGroupLocation :one
UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING *; UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING *;
-- name: DeleteTaskGroupByID :execrows
DELETE FROM task_group WHERE task_group_id = $1;

View File

@ -33,6 +33,7 @@
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
"react/prop-types": 0, "react/prop-types": 0,
"react/jsx-props-no-spreading": "off", "react/jsx-props-no-spreading": "off",
"no-param-reassign": "off",
"import/extensions": [ "import/extensions": [
"error", "error",
"ignorePackages", "ignorePackages",

View File

@ -39,6 +39,7 @@
"graphql": "^15.0.0", "graphql": "^15.0.0",
"graphql-tag": "^2.10.3", "graphql-tag": "^2.10.3",
"history": "^4.10.1", "history": "^4.10.1",
"immer": "^6.0.3",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.12.0", "react": "^16.12.0",

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import produce from 'immer';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import {
@ -9,6 +10,7 @@ import {
useUpdateTaskLocationMutation, useUpdateTaskLocationMutation,
useUpdateTaskGroupLocationMutation, useUpdateTaskGroupLocationMutation,
useCreateTaskGroupMutation, useCreateTaskGroupMutation,
useDeleteTaskGroupMutation,
} from 'shared/generated/graphql'; } from 'shared/generated/graphql';
import Navbar from 'App/Navbar'; import Navbar from 'App/Navbar';
@ -79,6 +81,26 @@ const Project = () => {
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState); const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
const [updateTaskLocation] = useUpdateTaskLocationMutation(); const [updateTaskLocation] = useUpdateTaskLocationMutation();
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation(); const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation();
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
onCompleted: deletedTaskGroupData => {
const nextState = produce(listsData, (draftState: State) => {
delete draftState.columns[deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID];
const filteredTasks = Object.keys(listsData.tasks)
.filter(
taskID =>
listsData.tasks[taskID].taskGroup.taskGroupID !==
deletedTaskGroupData.deleteTaskGroup.taskGroup.taskGroupID,
)
.reduce((obj: TaskState, key: string) => {
obj[key] = listsData.tasks[key];
return obj;
}, {});
draftState.tasks = filteredTasks;
});
setListsData(nextState);
},
});
const [createTaskGroup] = useCreateTaskGroupMutation({ const [createTaskGroup] = useCreateTaskGroupMutation({
onCompleted: newTaskGroupData => { onCompleted: newTaskGroupData => {
const newListsData = { const newListsData = {
@ -167,9 +189,14 @@ const Project = () => {
setListsData(newListsData); setListsData(newListsData);
}, },
}); });
const onCardDrop = (droppedTask: any) => { const onCardDrop = (droppedTask: Task) => {
console.log(droppedTask);
updateTaskLocation({ updateTaskLocation({
variables: { taskID: droppedTask.taskID, taskGroupID: droppedTask.taskGroupID, position: droppedTask.position }, variables: {
taskID: droppedTask.taskID,
taskGroupID: droppedTask.taskGroup.taskGroupID,
position: droppedTask.position,
},
}); });
const newState = { const newState = {
...listsData, ...listsData,
@ -229,6 +256,14 @@ const Project = () => {
</TitleWrapper> </TitleWrapper>
<Board> <Board>
<Lists <Lists
onExtraMenuOpen={(taskGroupID, pos, size) => {
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + size.height + 5,
taskGroupID,
});
}}
onQuickEditorOpen={onQuickEditorOpen} onQuickEditorOpen={onQuickEditorOpen}
onCardCreate={onCardCreate} onCardCreate={onCardCreate}
{...listsData} {...listsData}
@ -271,7 +306,13 @@ const Project = () => {
onClose={() => setPopupData(initialPopupState)} onClose={() => setPopupData(initialPopupState)}
left={popupData.left} left={popupData.left}
> >
<ListActions taskGroupID={popupData.taskGroupID} /> <ListActions
taskGroupID={popupData.taskGroupID}
onArchiveTaskGroup={taskGroupID => {
deleteTaskGroup({ variables: { taskGroupID } });
setPopupData(initialPopupState);
}}
/>
</PopupMenu> </PopupMenu>
)} )}
</> </>

View File

@ -1,3 +1,8 @@
interface DraggableElement {
id: string;
position: number;
}
type ContextMenuEvent = { type ContextMenuEvent = {
left: number; left: number;
top: number; top: number;

View File

@ -1,6 +1,8 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib'; import TextareaAutosize from 'react-autosize-textarea/lib';
export const Container = styled.div``;
export const Wrapper = styled.div<{ editorOpen: boolean }>` export const Wrapper = styled.div<{ editorOpen: boolean }>`
display: inline-block; display: inline-block;
background-color: hsla(0, 0%, 100%, 0.24); background-color: hsla(0, 0%, 100%, 0.24);

View File

@ -3,6 +3,7 @@ import { Plus, Cross } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { import {
Container,
Wrapper, Wrapper,
Placeholder, Placeholder,
AddIconWrapper, AddIconWrapper,
@ -79,26 +80,28 @@ const AddList: React.FC<AddListProps> = ({ onSave }) => {
useOnOutsideClick($wrapperRef, editorOpen, onOutsideClick, null); useOnOutsideClick($wrapperRef, editorOpen, onOutsideClick, null);
return ( return (
<Wrapper <Container>
ref={$wrapperRef} <Wrapper
editorOpen={editorOpen} ref={$wrapperRef}
onClick={() => { editorOpen={editorOpen}
if (!editorOpen) { onClick={() => {
setEditorOpen(true); if (!editorOpen) {
} setEditorOpen(true);
}} }
> }}
{editorOpen ? ( >
<NameEditor onCancel={() => setEditorOpen(false)} onSave={onSave} /> {editorOpen ? (
) : ( <NameEditor onCancel={() => setEditorOpen(false)} onSave={onSave} />
<Placeholder> ) : (
<AddIconWrapper> <Placeholder>
<Plus size={12} color="#c2c6dc" /> <AddIconWrapper>
</AddIconWrapper> <Plus size={12} color="#c2c6dc" />
Add another list </AddIconWrapper>
</Placeholder> Add another list
)} </Placeholder>
</Wrapper> )}
</Wrapper>
</Container>
); );
}; };

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions';
import ListActions from '.'; import ListActions from '.';
export default { export default {
@ -13,5 +14,5 @@ export default {
}; };
export const Default = () => { export const Default = () => {
return <ListActions taskGroupID="1" />; return <ListActions taskGroupID="1" onArchiveTaskGroup={action('on archive task group')} />;
}; };

View File

@ -3,8 +3,10 @@ import { ListActionsWrapper, ListActionItemWrapper, ListActionItem, ListSeparato
type Props = { type Props = {
taskGroupID: string; taskGroupID: string;
onArchiveTaskGroup: (taskGroupID: string) => void;
}; };
const LabelManager = ({ taskGroupID }: Props) => { const LabelManager: React.FC<Props> = ({ taskGroupID, onArchiveTaskGroup }) => {
return ( return (
<> <>
<ListActionsWrapper> <ListActionsWrapper>
@ -38,7 +40,7 @@ const LabelManager = ({ taskGroupID }: Props) => {
</ListActionsWrapper> </ListActionsWrapper>
<ListSeparator /> <ListSeparator />
<ListActionsWrapper> <ListActionsWrapper>
<ListActionItemWrapper> <ListActionItemWrapper onClick={() => onArchiveTaskGroup(taskGroupID)}>
<ListActionItem>Archive This List</ListActionItem> <ListActionItem>Archive This List</ListActionItem>
</ListActionItemWrapper> </ListActionItemWrapper>
</ListActionsWrapper> </ListActionsWrapper>

View File

@ -70,25 +70,28 @@ export const Default = () => {
...listsData, ...listsData,
tasks: { tasks: {
...listsData.tasks, ...listsData.tasks,
[droppedTask.id]: droppedTask, [droppedTask.taskGroupID]: droppedTask,
}, },
}; };
console.log(newState); console.log(newState);
setListsData(newState); setListsData(newState);
}; };
const onListDrop = (droppedColumn: any) => { const onListDrop = (droppedColumn: any) => {
console.log(droppedColumn);
const newState = { const newState = {
...listsData, ...listsData,
columns: { columns: {
...listsData.columns, ...listsData.columns,
[droppedColumn.id]: droppedColumn, [droppedColumn.taskGroupID]: droppedColumn,
}, },
}; };
console.log(newState);
setListsData(newState); setListsData(newState);
}; };
return ( return (
<Lists <Lists
{...listsData} {...listsData}
onExtraMenuOpen={action('extra menu open')}
onQuickEditorOpen={action('card composer open')} onQuickEditorOpen={action('card composer open')}
onCardDrop={onCardDrop} onCardDrop={onCardDrop}
onListDrop={onListDrop} onListDrop={onListDrop}
@ -203,6 +206,7 @@ export const ListsWithManyList = () => {
onCardDrop={onCardDrop} onCardDrop={onCardDrop}
onListDrop={onListDrop} onListDrop={onListDrop}
onCreateList={action('create list')} onCreateList={action('create list')}
onExtraMenuOpen={action('extra menu open')}
/> />
); );
}; };

View File

@ -1,7 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
export const Container = styled.div` export const Container = styled.div`
flex-grow: 1;
user-select: none; user-select: none;
white-space: nowrap; white-space: nowrap;
margin-bottom: 8px; margin-bottom: 8px;
@ -10,4 +9,7 @@ export const Container = styled.div`
padding-bottom: 8px; padding-bottom: 8px;
`; `;
export const BoardWrapper = styled.div`
display: flex;
`;
export default Container; export default Container;

View File

@ -11,7 +11,7 @@ import {
getAfterDropDraggableList, getAfterDropDraggableList,
} from 'shared/utils/draggables'; } from 'shared/utils/draggables';
import { Container } from './Styles'; import { Container, BoardWrapper } from './Styles';
interface Columns { interface Columns {
[key: string]: TaskGroup; [key: string]: TaskGroup;
@ -23,25 +23,56 @@ interface Tasks {
type Props = { type Props = {
columns: Columns; columns: Columns;
tasks: Tasks; tasks: Tasks;
onCardDrop: any; onCardDrop: (task: Task) => void;
onListDrop: any; onListDrop: (taskGroup: TaskGroup) => void;
onCardCreate: (taskGroupID: string, name: string) => void; onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void; onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void; onCreateList: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
}; };
const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEditorOpen, onCreateList }: Props) => { const Lists: React.FC<Props> = ({
columns,
tasks,
onCardDrop,
onListDrop,
onCardCreate,
onQuickEditorOpen,
onCreateList,
onExtraMenuOpen,
}) => {
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => { const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
if (typeof destination === 'undefined') return; if (typeof destination === 'undefined') return;
if (!isPositionChanged(source, destination)) return; if (!isPositionChanged(source, destination)) return;
const isList = type === 'column'; const isList = type === 'column';
const isSameList = destination.droppableId === source.droppableId; const isSameList = destination.droppableId === source.droppableId;
const droppedDraggable = isList ? columns[draggableId] : tasks[draggableId]; const droppedDraggable: DraggableElement = isList
? {
id: draggableId,
position: columns[draggableId].position,
}
: {
id: draggableId,
position: tasks[draggableId].position,
};
const beforeDropDraggables = isList const beforeDropDraggables = isList
? getSortedDraggables(Object.values(columns)) ? getSortedDraggables(
: getSortedDraggables(Object.values(tasks).filter((t: any) => t.taskGroupID === destination.droppableId)); Object.values(columns).map(column => {
return { id: column.taskGroupID, position: column.position };
}),
)
: getSortedDraggables(
Object.values(tasks)
.filter((t: any) => t.taskGroup.taskGroupID === destination.droppableId)
.map(task => {
return { id: task.taskID, position: task.position };
}),
);
console.log(beforeDropDraggables);
console.log(destination);
console.log(droppedDraggable);
const afterDropDraggables = getAfterDropDraggableList( const afterDropDraggables = getAfterDropDraggableList(
beforeDropDraggables, beforeDropDraggables,
droppedDraggable, droppedDraggable,
@ -49,16 +80,19 @@ const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEd
isSameList, isSameList,
destination, destination,
); );
console.log(afterDropDraggables);
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index); const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
if (isList) { if (isList) {
const droppedList = columns[droppedDraggable.id];
onListDrop({ onListDrop({
...droppedDraggable, ...droppedList,
position: newPosition, position: newPosition,
}); });
} else { } else {
const droppedCard = tasks[droppedDraggable.id];
const newCard = { const newCard = {
...droppedDraggable, ...droppedCard,
position: newPosition, position: newPosition,
taskGroupID: destination.droppableId, taskGroupID: destination.droppableId,
}; };
@ -66,89 +100,96 @@ const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEd
} }
}; };
const orderedColumns = getSortedDraggables(Object.values(columns)); const orderedColumns = getSortedDraggables(
Object.values(columns).map(column => {
return { id: column.taskGroupID, position: column.position };
}),
);
const [currentComposer, setCurrentComposer] = useState(''); const [currentComposer, setCurrentComposer] = useState('');
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <BoardWrapper>
<Droppable direction="horizontal" type="column" droppableId="root"> <DragDropContext onDragEnd={onDragEnd}>
{provided => ( <Droppable direction="horizontal" type="column" droppableId="root">
<Container {...provided.droppableProps} ref={provided.innerRef}> {provided => (
{orderedColumns.map((column: TaskGroup, index: number) => { <Container {...provided.droppableProps} ref={provided.innerRef}>
const columnCards = getSortedDraggables( {orderedColumns.map((columnDraggable, index: number) => {
Object.values(tasks).filter((t: Task) => t.taskGroup.taskGroupID === column.taskGroupID), const column = columns[columnDraggable.id];
); const columnCards = Object.values(tasks)
return ( .filter((t: Task) => t.taskGroup.taskGroupID === column.taskGroupID)
<Draggable draggableId={column.taskGroupID} key={column.taskGroupID} index={index}> .sort((a, b) => a.position - b.position);
{columnDragProvided => ( return (
<List <Draggable draggableId={column.taskGroupID} key={column.taskGroupID} index={index}>
id={column.taskGroupID} {columnDragProvided => (
name={column.name} <List
key={column.taskGroupID} id={column.taskGroupID}
onOpenComposer={id => setCurrentComposer(id)} name={column.name}
isComposerOpen={currentComposer === column.taskGroupID} key={column.taskGroupID}
onSaveName={name => console.log(name)} onOpenComposer={id => setCurrentComposer(id)}
index={index} isComposerOpen={currentComposer === column.taskGroupID}
tasks={columnCards} onSaveName={name => console.log(name)}
ref={columnDragProvided.innerRef} index={index}
wrapperProps={columnDragProvided.draggableProps} tasks={columnCards}
headerProps={columnDragProvided.dragHandleProps} ref={columnDragProvided.innerRef}
onExtraMenuOpen={(taskGroupID, pos, size) => console.log(taskGroupID, pos, size)} wrapperProps={columnDragProvided.draggableProps}
> headerProps={columnDragProvided.dragHandleProps}
<Droppable type="tasks" droppableId={column.taskGroupID}> onExtraMenuOpen={onExtraMenuOpen}
{columnDropProvided => ( >
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}> <Droppable type="tasks" droppableId={column.taskGroupID}>
{columnCards.map((task: Task, taskIndex: any) => { {columnDropProvided => (
return ( <ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
<Draggable key={task.taskID} draggableId={task.taskID} index={taskIndex}> {columnCards.map((task: Task, taskIndex: any) => {
{taskProvided => { return (
return ( <Draggable key={task.taskID} draggableId={task.taskID} index={taskIndex}>
<Card {taskProvided => {
wrapperProps={{ return (
...taskProvided.draggableProps, <Card
...taskProvided.dragHandleProps, wrapperProps={{
}} ...taskProvided.draggableProps,
ref={taskProvided.innerRef} ...taskProvided.dragHandleProps,
taskID={task.taskID} }}
taskGroupID={column.taskGroupID} ref={taskProvided.innerRef}
description="" taskID={task.taskID}
title={task.name} taskGroupID={column.taskGroupID}
labels={task.labels} description=""
onClick={e => console.log(e)} title={task.name}
onContextMenu={onQuickEditorOpen} labels={task.labels}
/> onClick={e => console.log(e)}
); onContextMenu={onQuickEditorOpen}
}} />
</Draggable> );
); }}
})} </Draggable>
{columnDropProvided.placeholder} );
})}
{columnDropProvided.placeholder}
{currentComposer === column.taskGroupID && ( {currentComposer === column.taskGroupID && (
<CardComposer <CardComposer
onClose={() => { onClose={() => {
setCurrentComposer(''); setCurrentComposer('');
}} }}
onCreateCard={name => { onCreateCard={name => {
onCardCreate(column.taskGroupID, name); onCardCreate(column.taskGroupID, name);
}} }}
isOpen isOpen
/> />
)} )}
</ListCards> </ListCards>
)} )}
</Droppable> </Droppable>
</List> </List>
)} )}
</Draggable> </Draggable>
); );
})} })}
{provided.placeholder} {provided.placeholder}
<AddList onSave={onCreateList} /> </Container>
</Container> )}
)} </Droppable>
</Droppable> </DragDropContext>
</DragDropContext> <AddList onSave={onCreateList} />
</BoardWrapper>
); );
}; };

View File

@ -77,7 +77,7 @@ export const ListActionsPopup = () => {
onClose={() => setPopupData(initalState)} onClose={() => setPopupData(initalState)}
left={popupData.left} left={popupData.left}
> >
<ListActions taskGroupID="1" /> <ListActions taskGroupID="1" onArchiveTaskGroup={action('archive task group')} />
</PopupMenu> </PopupMenu>
)} )}
<button <button

View File

@ -180,6 +180,17 @@ export type NewTaskGroupLocation = {
position: Scalars['Float']; position: Scalars['Float'];
}; };
export type DeleteTaskGroupInput = {
taskGroupID: Scalars['UUID'];
};
export type DeleteTaskGroupPayload = {
__typename?: 'DeleteTaskGroupPayload';
ok: Scalars['Boolean'];
affectedRows: Scalars['Int'];
taskGroup: TaskGroup;
};
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
createRefreshToken: RefreshToken; createRefreshToken: RefreshToken;
@ -189,11 +200,12 @@ export type Mutation = {
createProject: Project; createProject: Project;
createTaskGroup: TaskGroup; createTaskGroup: TaskGroup;
updateTaskGroupLocation: TaskGroup; updateTaskGroupLocation: TaskGroup;
deleteTaskGroup: DeleteTaskGroupPayload;
createTask: Task; createTask: Task;
updateTaskLocation: Task; updateTaskLocation: Task;
logoutUser: Scalars['Boolean'];
updateTaskName: Task; updateTaskName: Task;
deleteTask: DeleteTaskPayload; deleteTask: DeleteTaskPayload;
logoutUser: Scalars['Boolean'];
}; };
@ -232,6 +244,11 @@ export type MutationUpdateTaskGroupLocationArgs = {
}; };
export type MutationDeleteTaskGroupArgs = {
input: DeleteTaskGroupInput;
};
export type MutationCreateTaskArgs = { export type MutationCreateTaskArgs = {
input: NewTask; input: NewTask;
}; };
@ -242,11 +259,6 @@ export type MutationUpdateTaskLocationArgs = {
}; };
export type MutationLogoutUserArgs = {
input: LogoutUser;
};
export type MutationUpdateTaskNameArgs = { export type MutationUpdateTaskNameArgs = {
input: UpdateTaskName; input: UpdateTaskName;
}; };
@ -256,6 +268,11 @@ export type MutationDeleteTaskArgs = {
input: DeleteTaskInput; input: DeleteTaskInput;
}; };
export type MutationLogoutUserArgs = {
input: LogoutUser;
};
export type CreateTaskMutationVariables = { export type CreateTaskMutationVariables = {
taskGroupID: Scalars['String']; taskGroupID: Scalars['String'];
name: Scalars['String']; name: Scalars['String'];
@ -303,6 +320,27 @@ export type DeleteTaskMutation = (
) } ) }
); );
export type DeleteTaskGroupMutationVariables = {
taskGroupID: Scalars['UUID'];
};
export type DeleteTaskGroupMutation = (
{ __typename?: 'Mutation' }
& { deleteTaskGroup: (
{ __typename?: 'DeleteTaskGroupPayload' }
& Pick<DeleteTaskGroupPayload, 'ok' | 'affectedRows'>
& { taskGroup: (
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'taskGroupID'>
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name'>
)> }
) }
) }
);
export type FindProjectQueryVariables = { export type FindProjectQueryVariables = {
projectId: Scalars['String']; projectId: Scalars['String'];
}; };
@ -494,6 +532,46 @@ export function useDeleteTaskMutation(baseOptions?: ApolloReactHooks.MutationHoo
export type DeleteTaskMutationHookResult = ReturnType<typeof useDeleteTaskMutation>; export type DeleteTaskMutationHookResult = ReturnType<typeof useDeleteTaskMutation>;
export type DeleteTaskMutationResult = ApolloReactCommon.MutationResult<DeleteTaskMutation>; export type DeleteTaskMutationResult = ApolloReactCommon.MutationResult<DeleteTaskMutation>;
export type DeleteTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskMutation, DeleteTaskMutationVariables>; export type DeleteTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskMutation, DeleteTaskMutationVariables>;
export const DeleteTaskGroupDocument = gql`
mutation deleteTaskGroup($taskGroupID: UUID!) {
deleteTaskGroup(input: {taskGroupID: $taskGroupID}) {
ok
affectedRows
taskGroup {
taskGroupID
tasks {
taskID
name
}
}
}
}
`;
export type DeleteTaskGroupMutationFn = ApolloReactCommon.MutationFunction<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>;
/**
* __useDeleteTaskGroupMutation__
*
* To run a mutation, you first call `useDeleteTaskGroupMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteTaskGroupMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteTaskGroupMutation, { data, loading, error }] = useDeleteTaskGroupMutation({
* variables: {
* taskGroupID: // value for 'taskGroupID'
* },
* });
*/
export function useDeleteTaskGroupMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>) {
return ApolloReactHooks.useMutation<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>(DeleteTaskGroupDocument, baseOptions);
}
export type DeleteTaskGroupMutationHookResult = ReturnType<typeof useDeleteTaskGroupMutation>;
export type DeleteTaskGroupMutationResult = ApolloReactCommon.MutationResult<DeleteTaskGroupMutation>;
export type DeleteTaskGroupMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteTaskGroupMutation, DeleteTaskGroupMutationVariables>;
export const FindProjectDocument = gql` export const FindProjectDocument = gql`
query findProject($projectId: String!) { query findProject($projectId: String!) {
findProject(input: {projectId: $projectId}) { findProject(input: {projectId: $projectId}) {

View File

@ -0,0 +1,13 @@
mutation deleteTaskGroup($taskGroupID: UUID!) {
deleteTaskGroup(input: { taskGroupID: $taskGroupID }) {
ok
affectedRows
taskGroup {
taskGroupID
tasks {
taskID
name
}
}
}
}

View File

@ -1,22 +0,0 @@
export const moveItemWithinArray = (arr: any, item: any, newIndex: number) => {
const arrClone = [...arr];
const oldIndex = arrClone.indexOf(item);
arrClone.splice(newIndex, 0, arrClone.splice(oldIndex, 1)[0]);
return arrClone;
};
export const insertItemIntoArray = (arr: any, item: any, index: number) => {
const arrClone = [...arr];
arrClone.splice(index, 0, item);
return arrClone;
};
export const updateArrayItemById = (arr: any, itemId: any, fields: any) => {
const arrClone = [...arr];
const item = arrClone.find(({ id }) => id === itemId);
if (item) {
const itemIndex = arrClone.indexOf(item);
arrClone.splice(itemIndex, 1, { ...item, ...fields });
}
return arrClone;
};

View File

@ -1,26 +1,58 @@
import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/arrays'; import { DraggableLocation } from 'react-beautiful-dnd';
export const getNewDraggablePosition = (afterDropDraggables: any, draggableIndex: any) => { export const moveItemWithinArray = (arr: Array<DraggableElement>, item: DraggableElement, newIndex: number) => {
const arrClone = [...arr];
const oldIndex = arrClone.findIndex(i => i.id === item.id);
arrClone.splice(newIndex, 0, arrClone.splice(oldIndex, 1)[0]);
return arrClone;
};
export const insertItemIntoArray = (arr: Array<DraggableElement>, item: DraggableElement, index: number) => {
const arrClone = [...arr];
arrClone.splice(index, 0, item);
return arrClone;
};
export const updateArrayItemById = (arr: Array<DraggableElement>, itemId: string, fields: any) => {
const arrClone = [...arr];
const item = arrClone.find(({ id }) => id === itemId);
if (item) {
const itemIndex = arrClone.indexOf(item);
arrClone.splice(itemIndex, 1, { ...item, ...fields });
}
return arrClone;
};
export const getNewDraggablePosition = (afterDropDraggables: Array<DraggableElement>, draggableIndex: number) => {
const prevDraggable = afterDropDraggables[draggableIndex - 1]; const prevDraggable = afterDropDraggables[draggableIndex - 1];
const nextDraggable = afterDropDraggables[draggableIndex + 1]; const nextDraggable = afterDropDraggables[draggableIndex + 1];
if (!prevDraggable && !nextDraggable) { if (!prevDraggable && !nextDraggable) {
return 1; return 65535;
} }
if (!prevDraggable) { if (!prevDraggable) {
return nextDraggable.position - 1; console.log(
`in front of list [n/a : ${nextDraggable.id}]: ${nextDraggable.position} / 2.0 = ${nextDraggable.position / 2.0}`,
);
return nextDraggable.position / 2.0;
} }
if (!nextDraggable) { if (!nextDraggable) {
return prevDraggable.position + 1; console.log(
`end of list [${prevDraggable.id} : n/a] : ${prevDraggable.position} * 2.0 = ${prevDraggable.position * 2.0}`,
);
return prevDraggable.position * 2.0;
} }
const newPos = (prevDraggable.position + nextDraggable.position) / 2.0; const newPos = (prevDraggable.position + nextDraggable.position) / 2.0;
console.log(
`middle of two cards [${prevDraggable.id} : ${nextDraggable.id}] : ${prevDraggable.position} + ${nextDraggable.position} / 2.0 = ${newPos}`,
);
return newPos; return newPos;
}; };
export const getSortedDraggables = (draggables: any) => { export const getSortedDraggables = (draggables: Array<DraggableElement>) => {
return draggables.sort((a: any, b: any) => a.position - b.position); return draggables.sort((a: any, b: any) => a.position - b.position);
}; };
export const isPositionChanged = (source: any, destination: any) => { export const isPositionChanged = (source: DraggableLocation, destination: DraggableLocation) => {
if (!destination) return false; if (!destination) return false;
const isSameList = destination.droppableId === source.droppableId; const isSameList = destination.droppableId === source.droppableId;
const isSamePosition = destination.index === source.index; const isSamePosition = destination.index === source.index;
@ -28,11 +60,11 @@ export const isPositionChanged = (source: any, destination: any) => {
}; };
export const getAfterDropDraggableList = ( export const getAfterDropDraggableList = (
beforeDropDraggables: any, beforeDropDraggables: Array<DraggableElement>,
droppedDraggable: any, droppedDraggable: DraggableElement,
isList: any, isList: boolean,
isSameList: any, isSameList: boolean,
destination: any, destination: DraggableLocation,
) => { ) => {
if (isList) { if (isList) {
return moveItemWithinArray(beforeDropDraggables, droppedDraggable, destination.index); return moveItemWithinArray(beforeDropDraggables, droppedDraggable, destination.index);

View File

@ -8748,6 +8748,11 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
immer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/immer/-/immer-6.0.3.tgz#94d5051cd724668160a900d66d85ec02816f29bd"
integrity sha512-12VvNrfSrXZdm/BJgi/KDW2soq5freVSf3I1+4CLunUM8mAGx2/0Njy0xBVzi5zewQZiwM7z1/1T+8VaI7NkmQ==
immutable@~3.7.6: immutable@~3.7.6:
version "3.7.6" version "3.7.6"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"