add issue tasks.
This commit is contained in:
@@ -11,6 +11,7 @@ var ErrDriverNotSupported = errors.New("driver not found or supported")
|
||||
type Database interface {
|
||||
Activities() repositories.ActivityRepository
|
||||
Issues() repositories.IssueRepository
|
||||
IssueTasks() repositories.IssueTaskRepository
|
||||
Items() repositories.ItemRepository
|
||||
Projects() repositories.ProjectRepository
|
||||
Session() repositories.SessionRepository
|
||||
|
||||
@@ -16,6 +16,7 @@ type DB struct {
|
||||
db *sqlx.DB
|
||||
activities *activityRepository
|
||||
issues *issueRepository
|
||||
issueTasks *issueTaskRepository
|
||||
items *itemRepository
|
||||
projects *projectRepository
|
||||
sessions *sessionRepository
|
||||
@@ -31,6 +32,10 @@ func (db *DB) Issues() repositories.IssueRepository {
|
||||
return db.issues
|
||||
}
|
||||
|
||||
func (db *DB) IssueTasks() repositories.IssueTaskRepository {
|
||||
return db.issueTasks
|
||||
}
|
||||
|
||||
func (db *DB) Items() repositories.ItemRepository {
|
||||
return db.items
|
||||
}
|
||||
@@ -81,6 +86,7 @@ func Open(connectionString string) (*DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Setup repositories
|
||||
activities := &activityRepository{db: db}
|
||||
issues := &issueRepository{db: db}
|
||||
items := &itemRepository{db: db}
|
||||
@@ -88,11 +94,13 @@ func Open(connectionString string) (*DB, error) {
|
||||
users := &userRepository{db: db}
|
||||
sessions := &sessionRepository{db: db}
|
||||
projectStatuses := &projectStatusRepository{db: db}
|
||||
issueTasks := &issueTaskRepository{db: db}
|
||||
|
||||
return &DB{
|
||||
db: db,
|
||||
activities: activities,
|
||||
issues: issues,
|
||||
issueTasks: issueTasks,
|
||||
items: items,
|
||||
projects: projects,
|
||||
users: users,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package mysqldriver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"time"
|
||||
)
|
||||
|
||||
type issueTaskRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func (r *issueTaskRepository) Find(ctx context.Context, id string) (*models.IssueTask, error) {
|
||||
issueTask := models.IssueTask{}
|
||||
err := r.db.GetContext(ctx, &issueTask, "SELECT * FROM issue_task WHERE issue_task_id=?", id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, xlerrors.NotFound("Issue task")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &issueTask, nil
|
||||
}
|
||||
|
||||
func (r *issueTaskRepository) List(ctx context.Context, filter models.IssueTaskFilter) ([]*models.IssueTask, error) {
|
||||
q := sq.Select("*").From("issue_task")
|
||||
if filter.ActivityIDs != nil {
|
||||
q = q.Where(sq.Eq{"activity_id": filter.ActivityIDs})
|
||||
}
|
||||
if filter.IssueTaskIDs != nil {
|
||||
q = q.Where(sq.Eq{"issue_id": filter.IssueTaskIDs})
|
||||
}
|
||||
if filter.IssueIDs != nil {
|
||||
q = q.Where(sq.Eq{"issue_id": filter.IssueIDs})
|
||||
}
|
||||
if filter.MinStage != nil {
|
||||
q = q.Where(sq.GtOrEq{"status_stage": *filter.MinStage})
|
||||
}
|
||||
if filter.MaxStage != nil {
|
||||
q = q.Where(sq.LtOrEq{"status_stage": *filter.MaxStage})
|
||||
}
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*models.IssueTask, 0, 16)
|
||||
err = r.db.SelectContext(ctx, &results, query, args...)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return []*models.IssueTask{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *issueTaskRepository) Insert(ctx context.Context, task models.IssueTask) (*models.IssueTask, error) {
|
||||
if task.IssueID == "" {
|
||||
return nil, errors.New("missing issue id")
|
||||
}
|
||||
|
||||
if task.CreatedTime.IsZero() {
|
||||
task.CreatedTime = time.Now().Truncate(time.Second)
|
||||
task.UpdatedTime = task.CreatedTime
|
||||
}
|
||||
|
||||
tx, err := r.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nextID, err := incCounter(ctx, tx, counterKindIssueSubID, task.IssueID)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
task.ID = fmt.Sprintf("%s-%d", task.IssueID, nextID)
|
||||
|
||||
_, err = tx.NamedExecContext(ctx, `
|
||||
INSERT INTO issue_task (
|
||||
issue_task_id, issue_id, activity_id, created_time,
|
||||
updated_time, due_time, status_stage, status_name,
|
||||
name, description, estimated_time, estimated_units,
|
||||
points_multiplier
|
||||
) VALUES (
|
||||
:issue_task_id, :issue_id, :activity_id, :created_time,
|
||||
:updated_time, :due_time, :status_stage, :status_name,
|
||||
:name, :description, :estimated_time, :estimated_units,
|
||||
:points_multiplier
|
||||
)
|
||||
`, task)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (r *issueTaskRepository) Save(ctx context.Context, task models.IssueTask) error {
|
||||
_, err := r.db.NamedExecContext(ctx, `
|
||||
UPDATE issue_task SET
|
||||
due_time=:due_time,
|
||||
status_stage=:status_stage,
|
||||
status_name=:status_name,
|
||||
name=:name,
|
||||
description=:description,
|
||||
estimated_time=:estimated_time,
|
||||
estimated_units=:estimated_units,
|
||||
points_multiplier=:points_multiplier
|
||||
WHERE issue_task_id=:issue_task_id
|
||||
`, task)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *issueTaskRepository) Delete(ctx context.Context, task models.IssueTask) error {
|
||||
_, err := r.db.ExecContext(ctx, "DELETE FROM issue_task WHERE issue_task_id=? LIMIT 1;", task.ID)
|
||||
return err
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
)
|
||||
|
||||
var counterKindIssueID = "NextIssueID"
|
||||
var counterKindIssueSubID = "NextIssueSubID"
|
||||
|
||||
type issueRepository struct {
|
||||
db *sqlx.DB
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
type IssueTaskRepository interface {
|
||||
Find(ctx context.Context, id string) (*models.IssueTask, error)
|
||||
List(ctx context.Context, filter models.IssueTaskFilter) ([]*models.IssueTask, error)
|
||||
Insert(ctx context.Context, task models.IssueTask) (*models.IssueTask, error)
|
||||
Save(ctx context.Context, task models.IssueTask) error
|
||||
Delete(ctx context.Context, task models.IssueTask) error
|
||||
}
|
||||
@@ -14,6 +14,12 @@ models:
|
||||
fields:
|
||||
unitName:
|
||||
resolver: true
|
||||
IssueTask:
|
||||
fields:
|
||||
estimatedUnits:
|
||||
resolver: true
|
||||
Duration:
|
||||
model: git.aiterp.net/stufflog/server/graph/scalars.Duration
|
||||
|
||||
resolver:
|
||||
layout: follow-schema
|
||||
|
||||
@@ -5,6 +5,7 @@ package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
@@ -76,6 +76,15 @@ func (r *issueResolver) Status(ctx context.Context, obj *models.Issue) (*models.
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (r *issueResolver) Tasks(ctx context.Context, obj *models.Issue, filter *models.IssueTaskFilter) ([]*models.IssueTask, error) {
|
||||
if filter == nil {
|
||||
filter = &models.IssueTaskFilter{}
|
||||
}
|
||||
filter.IssueTaskIDs = []string{obj.ID}
|
||||
|
||||
return r.Database.IssueTasks().List(ctx, *filter)
|
||||
}
|
||||
|
||||
// Issue returns graphcore.IssueResolver implementation.
|
||||
func (r *Resolver) Issue() graphcore.IssueResolver { return &issueResolver{r} }
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package resolvers
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/graph/graphutil"
|
||||
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||
"git.aiterp.net/stufflog/server/models"
|
||||
)
|
||||
|
||||
func (r *issueTaskResolver) EstimatedUnits(ctx context.Context, obj *models.IssueTask) (*int, error) {
|
||||
// TODO: Data loader
|
||||
activity, err := r.Database.Activities().Find(ctx, obj.ActivityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !activity.Countable || activity.UnitIsTimeSpent {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &obj.EstimatedUnits, nil
|
||||
}
|
||||
|
||||
func (r *issueTaskResolver) Issue(ctx context.Context, obj *models.IssueTask) (*models.Issue, error) {
|
||||
return r.Database.Issues().Find(ctx, obj.IssueID)
|
||||
}
|
||||
|
||||
func (r *issueTaskResolver) Activity(ctx context.Context, obj *models.IssueTask) (*models.Activity, error) {
|
||||
return r.Database.Activities().Find(ctx, obj.ActivityID)
|
||||
}
|
||||
|
||||
func (r *issueTaskResolver) Status(ctx context.Context, obj *models.IssueTask) (*models.ProjectStatus, error) {
|
||||
// The project ID is always the prefix before the first dash in the issue ID.
|
||||
split := strings.SplitN(obj.IssueID, "-", 2)
|
||||
if len(split) == 0 {
|
||||
return nil, errors.New("invalid issue ID")
|
||||
}
|
||||
projectID := split[0]
|
||||
|
||||
// Shortcut: if description isn't needed, resolve this with issue's properties.
|
||||
if !graphutil.SelectsAnyField(ctx, "description") {
|
||||
return &models.ProjectStatus{
|
||||
ProjectID: projectID,
|
||||
Stage: obj.StatusStage,
|
||||
Name: obj.StatusName,
|
||||
Description: "FAKE",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Find it in the database. TODO: DataLoader
|
||||
status, err := r.Database.ProjectStatuses().Find(ctx, projectID, obj.StatusName)
|
||||
if xlerrors.IsNotFound(err) {
|
||||
return &models.ProjectStatus{
|
||||
ProjectID: projectID,
|
||||
Stage: obj.StatusStage,
|
||||
Name: obj.StatusName,
|
||||
Description: "(Deleted or unknown status)",
|
||||
}, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the stage doesn't match, sneakily correct it for next time.
|
||||
if status.Stage != obj.StatusStage {
|
||||
updatedTask := *obj
|
||||
updatedTask.StatusStage = status.Stage
|
||||
_ = r.Database.IssueTasks().Save(ctx, updatedTask)
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (r *issueTaskResolver) RemainingTime(ctx context.Context, obj *models.IssueTask) (time.Duration, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
func (r *issueTaskResolver) RemainingUnits(ctx context.Context, obj *models.IssueTask) (*int, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
|
||||
// IssueTask returns graphcore.IssueTaskResolver implementation.
|
||||
func (r *Resolver) IssueTask() graphcore.IssueTaskResolver { return &issueTaskResolver{r} }
|
||||
|
||||
type issueTaskResolver struct{ *Resolver }
|
||||
@@ -6,6 +6,7 @@ package resolvers
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||
@@ -169,6 +170,120 @@ func (r *mutationResolver) CreateIssue(ctx context.Context, input graphcore.Issu
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateIssueTask(ctx context.Context, input graphcore.IssueTaskCreateInput) (*models.IssueTask, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
issue, err := r.Database.Issues().Find(ctx, input.IssueID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if perm, err := r.Auth.IssuePermission(ctx, *issue); err != nil || !perm.CanManageOwnIssue() {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
status, err := r.Database.ProjectStatuses().Find(ctx, issue.ProjectID, input.StatusName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activity, err := r.Database.Activities().Find(ctx, input.ActivityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if activity.ProjectID != issue.ProjectID {
|
||||
return nil, xlerrors.NotFound("Activity")
|
||||
}
|
||||
|
||||
issueTask := &models.IssueTask{
|
||||
IssueID: issue.ID,
|
||||
ActivityID: activity.ID,
|
||||
CreatedTime: time.Now(),
|
||||
UpdatedTime: time.Now(),
|
||||
StatusStage: status.Stage,
|
||||
StatusName: status.Name,
|
||||
Name: input.Name,
|
||||
Description: input.Description,
|
||||
PointsMultiplier: 1.0,
|
||||
}
|
||||
if input.EstimatedUnits != nil && activity.Countable && !activity.UnitIsTimeSpent {
|
||||
issueTask.EstimatedUnits = *input.EstimatedUnits
|
||||
}
|
||||
if input.PointsMultiplier != nil && *input.PointsMultiplier > 0 {
|
||||
issueTask.PointsMultiplier = *input.PointsMultiplier
|
||||
}
|
||||
|
||||
issueTask, err = r.Database.IssueTasks().Insert(ctx, *issueTask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return issueTask, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EditIssueTask(ctx context.Context, input graphcore.IssueTaskEditInput) (*models.IssueTask, error) {
|
||||
user := r.Auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
task, err := r.Database.IssueTasks().Find(ctx, input.IssueTaskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issue, err := r.Database.Issues().Find(ctx, task.IssueID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if perm, err := r.Auth.IssuePermission(ctx, *issue); err != nil || !perm.CanManageOwnIssue() {
|
||||
return nil, xlerrors.PermissionDenied
|
||||
}
|
||||
|
||||
if input.SetName != nil {
|
||||
task.Name = *input.SetName
|
||||
}
|
||||
if input.SetDescription != nil {
|
||||
task.Description = *input.SetDescription
|
||||
}
|
||||
if input.SetDueTime != nil {
|
||||
task.DueTime = input.SetDueTime
|
||||
}
|
||||
if input.SetEstimatedTime != nil {
|
||||
task.EstimatedTime = *input.SetEstimatedTime
|
||||
}
|
||||
if input.SetEstimatedUnits != nil {
|
||||
activity, err := r.Database.Activities().Find(ctx, task.ActivityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if activity.Countable && !activity.UnitIsTimeSpent {
|
||||
task.EstimatedUnits = *input.SetEstimatedUnits
|
||||
}
|
||||
}
|
||||
if input.SetPointsMultiplier != nil && *input.SetPointsMultiplier > 0 {
|
||||
task.PointsMultiplier = *input.SetPointsMultiplier
|
||||
}
|
||||
if input.SetStatusName != nil {
|
||||
status, err := r.Database.ProjectStatuses().Find(ctx, issue.ProjectID, *input.SetStatusName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.StatusName = status.Name
|
||||
task.StatusStage = status.Stage
|
||||
}
|
||||
|
||||
err = r.Database.IssueTasks().Save(ctx, *task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) LoginUser(ctx context.Context, input graphcore.UserLoginInput) (*models.User, error) {
|
||||
return r.Auth.Login(ctx, input.Username, input.Password)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package scalars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func MarshalDuration(d time.Duration) graphql.Marshaler {
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
_, _ = w.Write([]byte(strconv.Itoa(int(d.Milliseconds()))))
|
||||
})
|
||||
}
|
||||
|
||||
func UnmarshalDuration(v interface{}) (time.Duration, error) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return time.ParseDuration(v)
|
||||
case int:
|
||||
return time.Millisecond * time.Duration(v), nil
|
||||
case int64:
|
||||
return time.Millisecond * time.Duration(v), nil
|
||||
case float64:
|
||||
return time.Millisecond * time.Duration(v), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("%T is not a bool", v)
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,14 @@ type Issue {
|
||||
project: Project
|
||||
"The issue's owner/creator."
|
||||
owner: User
|
||||
"The issue assignee."
|
||||
"The issue's assignee."
|
||||
assignee: User
|
||||
"The issue status."
|
||||
"Current status of the issue."
|
||||
status: ProjectStatus!
|
||||
"Issue tasks."
|
||||
tasks(filter: IssueTaskFilter): [IssueTask!]!
|
||||
#"Logs related to this issue."
|
||||
#logs: [Log!]!
|
||||
}
|
||||
|
||||
input IssueFilter {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
An issue task is a the main part of an issue. They contain estimates, and which activity
|
||||
is to be performed. They don't get their own page in UI and should have all their information
|
||||
presented in a concise manner.
|
||||
"""
|
||||
type IssueTask {
|
||||
"The issue task ID."
|
||||
id: String!
|
||||
"The time when the task was created."
|
||||
createdTime: Time!
|
||||
"The time when the task was updated."
|
||||
updatedTime: Time!
|
||||
"The time when the task is due."
|
||||
dueTime: Time
|
||||
"A name for the task."
|
||||
name: String!
|
||||
"A short description of the task."
|
||||
description: String!
|
||||
"The estimated time."
|
||||
estimatedTime: Duration!
|
||||
"The estimated units."
|
||||
estimatedUnits: Int
|
||||
"A multiplier for the points earned."
|
||||
pointsMultiplier: Float!
|
||||
|
||||
"Parent issue."
|
||||
issue: Issue!
|
||||
"Activity the task performs."
|
||||
activity: Activity!
|
||||
"The status of this task."
|
||||
status: ProjectStatus!
|
||||
#"Logs related to this task."
|
||||
#logs: [Log!]!
|
||||
|
||||
"Remaining time from the logs."
|
||||
remainingTime: Duration!
|
||||
"Remaining units (if countable)"
|
||||
remainingUnits: Int
|
||||
}
|
||||
|
||||
"""
|
||||
A subset of the filter for Issue.tasks
|
||||
"""
|
||||
input IssueTaskFilter {
|
||||
"The activity IDs to limit the task list with."
|
||||
activityIds: [String!]
|
||||
"The lowest stage (inclusive)."
|
||||
minStage: Int
|
||||
"The highest stage (inclusive)."
|
||||
maxStage: Int
|
||||
}
|
||||
|
||||
"""
|
||||
Input for the createIssueTask mutation.
|
||||
"""
|
||||
input IssueTaskCreateInput {
|
||||
"The issue ID to parent to."
|
||||
issueId: String!
|
||||
|
||||
"The activity ID this task is about."
|
||||
activityId: String!
|
||||
"The name of the task."
|
||||
name: String!
|
||||
"The description of the task."
|
||||
description: String!
|
||||
"Estimated time to perform the task."
|
||||
estimatedTime: Duration!
|
||||
"Task status."
|
||||
statusName: String!
|
||||
|
||||
"Estimate an amount of units. This is required for issues with a countable activity."
|
||||
estimatedUnits: Int
|
||||
"Set an optional multiplier for the issue."
|
||||
pointsMultiplier: Float
|
||||
}
|
||||
|
||||
"""
|
||||
Input for the editIssueTask mutation.
|
||||
"""
|
||||
input IssueTaskEditInput {
|
||||
"The issue task to edit."
|
||||
issueTaskId: String!
|
||||
|
||||
"Update the status."
|
||||
setStatusName: String
|
||||
"Set the name."
|
||||
setName: String
|
||||
"Set description."
|
||||
setDescription: String
|
||||
"Set estimated time."
|
||||
setEstimatedTime: Duration
|
||||
"Set estimated units."
|
||||
setEstimatedUnits: Int
|
||||
"Set points multiplier."
|
||||
setPointsMultiplier: Float
|
||||
"Set due time."
|
||||
setDueTime: Time
|
||||
}
|
||||
@@ -9,9 +9,19 @@ type Mutation {
|
||||
"Edit an activity."
|
||||
editActivity(input: ActivityEditInput!): Activity!
|
||||
|
||||
# ITEM
|
||||
|
||||
# ISSUE
|
||||
"Create a new issue."
|
||||
createIssue(input: IssueCreateInput!): Issue!
|
||||
# ISSUE TASK
|
||||
"Create a new issue task."
|
||||
createIssueTask(input: IssueTaskCreateInput!): IssueTask!
|
||||
"Edit an issue task."
|
||||
editIssueTask(input: IssueTaskEditInput!): IssueTask!
|
||||
# ISSUE ITEM
|
||||
|
||||
# LOG
|
||||
|
||||
# USER
|
||||
"Log in."
|
||||
|
||||
@@ -29,6 +29,7 @@ type ProjectPermission {
|
||||
user: User
|
||||
}
|
||||
|
||||
"The project's statuses for issues and issue tasks."
|
||||
type ProjectStatus {
|
||||
"The stage of the status. 0=inactive, 1=pending, 2=active, 3=review, 4=completed, 5=failed, 6=postponed"
|
||||
stage: Int!
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
"""
|
||||
A ISO3339 timestamp.
|
||||
"""
|
||||
scalar Time
|
||||
|
||||
"""
|
||||
Duration in milliseconds. It will output as an integer, but can be supplied as a float, integer
|
||||
or a string accepted by the Go time.Duration parser.
|
||||
"""
|
||||
scalar Duration
|
||||
@@ -2,7 +2,7 @@
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE project_status (
|
||||
project_id CHAR(16) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
name CHAR(32) NOT NULL,
|
||||
stage INT NOT NULL,
|
||||
description VARCHAR(255) NOT NULL,
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE issue_task (
|
||||
issue_task_id CHAR(48) PRIMARY KEY,
|
||||
issue_id CHAR(32) NOT NULL,
|
||||
activity_id CHAR(16) NOT NULL,
|
||||
created_time TIMESTAMP NOT NULL,
|
||||
updated_time TIMESTAMP NOT NULL,
|
||||
due_time TIMESTAMP,
|
||||
status_stage INT NOT NULL,
|
||||
status_name CHAR(32) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
estimated_time BIGINT NOT NULL,
|
||||
estimated_units INT NOT NULL,
|
||||
points_multiplier FLOAT NOT NULL,
|
||||
|
||||
INDEX (issue_id),
|
||||
INDEX (activity_id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE issue_task;
|
||||
-- +goose StatementEnd
|
||||
+10
-2
@@ -4,12 +4,12 @@ import "time"
|
||||
|
||||
// An IssueTask is a task within an issue.
|
||||
type IssueTask struct {
|
||||
TaskID string `db:"task_id"`
|
||||
ID string `db:"issue_task_id"`
|
||||
IssueID string `db:"issue_id"`
|
||||
ActivityID string `db:"activity_id"`
|
||||
CreatedTime time.Time `db:"created_time"`
|
||||
UpdatedTime time.Time `db:"updated_time"`
|
||||
DueTime time.Time `db:"due_time"`
|
||||
DueTime *time.Time `db:"due_time"`
|
||||
StatusStage int `db:"status_stage"`
|
||||
StatusName string `db:"status_name"`
|
||||
Name string `db:"name"`
|
||||
@@ -18,3 +18,11 @@ type IssueTask struct {
|
||||
EstimatedUnits int `db:"estimated_units"`
|
||||
PointsMultiplier float64 `db:"points_multiplier"`
|
||||
}
|
||||
|
||||
type IssueTaskFilter struct {
|
||||
IssueTaskIDs []string
|
||||
IssueIDs []string
|
||||
ActivityIDs []string
|
||||
MinStage *int
|
||||
MaxStage *int
|
||||
}
|
||||
|
||||
@@ -13,10 +13,3 @@ type ItemFilter struct {
|
||||
ItemIDs []string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
/*
|
||||
SELECT i.item_id, i.name FROM item i
|
||||
LEFT JOIN tag AS t ON t.item_id = i.item_id
|
||||
WHERE t.tag_name IN ("Groceries")
|
||||
GROUP by i.item_id;
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Log struct {
|
||||
ID string `db:"log_id"`
|
||||
IssueID string `db:"issue_id"`
|
||||
UserID string `db:"user_id"`
|
||||
IssueTaskID *string `db:"issue_task_id"`
|
||||
UnitsDone *int `db:"units_done"`
|
||||
Duration time.Duration `db:"duration"`
|
||||
Description []string `db:"description"`
|
||||
}
|
||||
|
||||
type LogFilter struct {
|
||||
LogIDs []string
|
||||
IssueIDs []string
|
||||
IssueTaskIDs []string
|
||||
}
|
||||
|
||||
type LogItem struct {
|
||||
LogID string `db:"log_id"`
|
||||
IssueItemID string `db:"issue_item_id"`
|
||||
Amount int `db:"amount"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user