add activities.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
var ErrDriverNotSupported = errors.New("driver not found or supported")
|
var ErrDriverNotSupported = errors.New("driver not found or supported")
|
||||||
|
|
||||||
type Database interface {
|
type Database interface {
|
||||||
|
Activities() repositories.ActivityRepository
|
||||||
Issues() repositories.IssueRepository
|
Issues() repositories.IssueRepository
|
||||||
Items() repositories.ItemRepository
|
Items() repositories.ItemRepository
|
||||||
Projects() repositories.ProjectRepository
|
Projects() repositories.ProjectRepository
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package mysqldriver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"git.aiterp.net/stufflog/server/internal/generate"
|
||||||
|
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||||
|
"git.aiterp.net/stufflog/server/models"
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activityRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *activityRepository) Find(ctx context.Context, id string) (*models.Activity, error) {
|
||||||
|
activity := models.Activity{}
|
||||||
|
err := r.db.GetContext(ctx, &activity, "SELECT * FROM activity WHERE activity_id=?", id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, xlerrors.NotFound("Activity")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *activityRepository) List(ctx context.Context, filter models.ActivityFilter) ([]*models.Activity, error) {
|
||||||
|
q := sq.Select("*").From("activity")
|
||||||
|
if len(filter.ActivityIDs) > 0 {
|
||||||
|
q = q.Where(sq.Eq{"activity_id": filter.ActivityIDs})
|
||||||
|
}
|
||||||
|
if len(filter.ProjectIDs) > 0 {
|
||||||
|
q = q.Where(sq.Eq{"project_id": filter.ProjectIDs})
|
||||||
|
}
|
||||||
|
|
||||||
|
query, args, err := q.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]*models.Activity, 0, 16)
|
||||||
|
err = r.db.SelectContext(ctx, &results, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return []*models.Activity{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *activityRepository) Insert(ctx context.Context, activity models.Activity) (*models.Activity, error) {
|
||||||
|
if activity.ProjectID == "" {
|
||||||
|
return nil, errors.New("missing project id")
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.ID = generate.ActivityID()
|
||||||
|
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
INSERT INTO activity (
|
||||||
|
activity_id, project_id, name, countable,
|
||||||
|
unit_is_time, unit_name, unit_value, base_value
|
||||||
|
) VALUES (
|
||||||
|
:activity_id, :project_id, :name, :countable,
|
||||||
|
:unit_is_time, :unit_name, :unit_value, :base_value
|
||||||
|
)
|
||||||
|
`, activity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *activityRepository) Save(ctx context.Context, activity models.Activity) error {
|
||||||
|
_, err := r.db.NamedExecContext(ctx, `
|
||||||
|
UPDATE activity SET
|
||||||
|
name=:name,
|
||||||
|
countable=:countable,
|
||||||
|
unit_is_time=:unit_is_time,
|
||||||
|
unit_name=:unit_name,
|
||||||
|
unit_value=:unit_value,
|
||||||
|
base_value=:base_value
|
||||||
|
WHERE activity_id=:activity_id
|
||||||
|
`, activity)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *activityRepository) Delete(ctx context.Context, activity models.Activity) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, "DELETE FROM activity WHERE activity_id=? LIMIT 1;", activity.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
|
activities *activityRepository
|
||||||
issues *issueRepository
|
issues *issueRepository
|
||||||
items *itemRepository
|
items *itemRepository
|
||||||
projects *projectRepository
|
projects *projectRepository
|
||||||
@@ -22,6 +23,10 @@ type DB struct {
|
|||||||
projectStatuses *projectStatusRepository
|
projectStatuses *projectStatusRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) Activities() repositories.ActivityRepository {
|
||||||
|
return db.activities
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) Issues() repositories.IssueRepository {
|
func (db *DB) Issues() repositories.IssueRepository {
|
||||||
return db.issues
|
return db.issues
|
||||||
}
|
}
|
||||||
@@ -76,6 +81,7 @@ func Open(connectionString string) (*DB, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activities := &activityRepository{db: db}
|
||||||
issues := &issueRepository{db: db}
|
issues := &issueRepository{db: db}
|
||||||
items := &itemRepository{db: db}
|
items := &itemRepository{db: db}
|
||||||
projects := &projectRepository{db: db}
|
projects := &projectRepository{db: db}
|
||||||
@@ -85,6 +91,7 @@ func Open(connectionString string) (*DB, error) {
|
|||||||
|
|
||||||
return &DB{
|
return &DB{
|
||||||
db: db,
|
db: db,
|
||||||
|
activities: activities,
|
||||||
issues: issues,
|
issues: issues,
|
||||||
items: items,
|
items: items,
|
||||||
projects: projects,
|
projects: projects,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.aiterp.net/stufflog/server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityRepository interface {
|
||||||
|
Find(ctx context.Context, id string) (*models.Activity, error)
|
||||||
|
List(ctx context.Context, filter models.ActivityFilter) ([]*models.Activity, error)
|
||||||
|
Insert(ctx context.Context, activity models.Activity) (*models.Activity, error)
|
||||||
|
Save(ctx context.Context, activity models.Activity) error
|
||||||
|
Delete(ctx context.Context, activity models.Activity) error
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ model:
|
|||||||
filename: graphcore/input_gen.go
|
filename: graphcore/input_gen.go
|
||||||
package: graphcore
|
package: graphcore
|
||||||
|
|
||||||
|
models:
|
||||||
|
Activity:
|
||||||
|
fields:
|
||||||
|
unitName:
|
||||||
|
resolver: true
|
||||||
|
|
||||||
resolver:
|
resolver:
|
||||||
layout: follow-schema
|
layout: follow-schema
|
||||||
dir: resolvers
|
dir: resolvers
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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"
|
||||||
|
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||||
|
"git.aiterp.net/stufflog/server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *activityResolver) UnitName(ctx context.Context, obj *models.Activity) (*string, error) {
|
||||||
|
if !obj.Countable || obj.UnitIsTimeSpent {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj.UnitName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *activityResolver) Project(ctx context.Context, obj *models.Activity) (*models.Project, error) {
|
||||||
|
return r.Database.Projects().Find(ctx, obj.ProjectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activity returns graphcore.ActivityResolver implementation.
|
||||||
|
func (r *Resolver) Activity() graphcore.ActivityResolver { return &activityResolver{r} }
|
||||||
|
|
||||||
|
type activityResolver struct{ *Resolver }
|
||||||
@@ -5,6 +5,7 @@ package resolvers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||||
"git.aiterp.net/stufflog/server/graph/graphutil"
|
"git.aiterp.net/stufflog/server/graph/graphutil"
|
||||||
"git.aiterp.net/stufflog/server/graph/loaders"
|
"git.aiterp.net/stufflog/server/graph/loaders"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package resolvers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||||
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||||
"git.aiterp.net/stufflog/server/models"
|
"git.aiterp.net/stufflog/server/models"
|
||||||
@@ -44,6 +45,83 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input graphcore.Pr
|
|||||||
return project, nil
|
return project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) CreateActivity(ctx context.Context, input graphcore.ActivityCreateInput) (*models.Activity, error) {
|
||||||
|
user := r.Auth.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil, xlerrors.PermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := r.Database.Projects().Find(ctx, input.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if perm, err := r.Auth.ProjectPermission(ctx, *project); err != nil || !perm.CanManageActivities() {
|
||||||
|
return nil, xlerrors.PermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
activity := &models.Activity{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
Name: input.Name,
|
||||||
|
Countable: input.Countable != nil && *input.Countable,
|
||||||
|
UnitIsTimeSpent: input.UnitIsTimeSpent != nil && *input.UnitIsTimeSpent,
|
||||||
|
BaseValue: input.BaseValue,
|
||||||
|
}
|
||||||
|
if !activity.UnitIsTimeSpent && activity.Countable {
|
||||||
|
if input.UnitName != nil {
|
||||||
|
activity.UnitName = *input.UnitName
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("unit name is required for countable non-time-spent activities")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if activity.UnitIsTimeSpent || activity.Countable {
|
||||||
|
if input.UnitValue != nil {
|
||||||
|
activity.UnitValue = *input.UnitValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Database.Activities().Insert(ctx, *activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) EditActivity(ctx context.Context, input graphcore.ActivityEditInput) (*models.Activity, error) {
|
||||||
|
user := r.Auth.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil, xlerrors.PermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
activity, err := r.Database.Activities().Find(ctx, input.ActivityID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := r.Database.Projects().Find(ctx, activity.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if perm, err := r.Auth.ProjectPermission(ctx, *project); err != nil || !perm.CanManageActivities() {
|
||||||
|
return nil, xlerrors.PermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.SetName != nil {
|
||||||
|
activity.Name = *input.SetName
|
||||||
|
}
|
||||||
|
if input.SetBaseValue != nil {
|
||||||
|
activity.BaseValue = *input.SetBaseValue
|
||||||
|
}
|
||||||
|
if input.SetUnitName != nil {
|
||||||
|
activity.UnitName = *input.SetUnitName
|
||||||
|
}
|
||||||
|
if input.SetUnitValue != nil {
|
||||||
|
activity.UnitValue = *input.SetUnitValue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Database.Activities().Save(ctx, *activity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateIssue(ctx context.Context, input graphcore.IssueCreateInput) (*models.Issue, error) {
|
func (r *mutationResolver) CreateIssue(ctx context.Context, input graphcore.IssueCreateInput) (*models.Issue, error) {
|
||||||
user := r.Auth.UserFromContext(ctx)
|
user := r.Auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
@@ -58,14 +136,19 @@ func (r *mutationResolver) CreateIssue(ctx context.Context, input graphcore.Issu
|
|||||||
return nil, xlerrors.PermissionDenied
|
return nil, xlerrors.PermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status, err := r.Database.ProjectStatuses().Find(ctx, project.ID, input.StatusName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
issue := &models.Issue{
|
issue := &models.Issue{
|
||||||
ProjectID: project.ID,
|
ProjectID: project.ID,
|
||||||
OwnerID: user.ID,
|
OwnerID: user.ID,
|
||||||
AssigneeID: "",
|
AssigneeID: "",
|
||||||
StatusStage: input.StatusStage,
|
StatusStage: status.Stage,
|
||||||
StatusName: input.StatusName,
|
StatusName: status.Name,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
Title: input.Name,
|
Title: input.Name, // Title set below if it's in the input.
|
||||||
Description: input.Description,
|
Description: input.Description,
|
||||||
}
|
}
|
||||||
if input.Title != nil && *input.Title != "" {
|
if input.Title != nil && *input.Title != "" {
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ func (r *projectResolver) Statuses(ctx context.Context, obj *models.Project, fil
|
|||||||
return r.Database.ProjectStatuses().List(ctx, *filter)
|
return r.Database.ProjectStatuses().List(ctx, *filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectResolver) Activities(ctx context.Context, obj *models.Project) ([]*models.Activity, error) {
|
||||||
|
return r.Database.Activities().List(ctx, models.ActivityFilter{ProjectIDs: []string{obj.ID}})
|
||||||
|
}
|
||||||
|
|
||||||
func (r *projectPermissionResolver) User(ctx context.Context, obj *models.ProjectPermission) (*models.User, error) {
|
func (r *projectPermissionResolver) User(ctx context.Context, obj *models.ProjectPermission) (*models.User, error) {
|
||||||
return loaders.UserLoaderFromContext(ctx).Load(obj.UserID)
|
return loaders.UserLoaderFromContext(ctx).Load(obj.UserID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"git.aiterp.net/stufflog/server/graph/graphcore"
|
"git.aiterp.net/stufflog/server/graph/graphcore"
|
||||||
|
"git.aiterp.net/stufflog/server/internal/xlerrors"
|
||||||
"git.aiterp.net/stufflog/server/models"
|
"git.aiterp.net/stufflog/server/models"
|
||||||
"git.aiterp.net/stufflog/server/services"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) Issue(ctx context.Context, id string) (*models.Issue, error) {
|
func (r *queryResolver) Issue(ctx context.Context, id string) (*models.Issue, error) {
|
||||||
user := r.Auth.UserFromContext(ctx)
|
user := r.Auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, services.ErrPermissionDenied
|
return nil, xlerrors.PermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
issue, err := r.Database.Issues().Find(ctx, id)
|
issue, err := r.Database.Issues().Find(ctx, id)
|
||||||
@@ -33,7 +33,7 @@ func (r *queryResolver) Issue(ctx context.Context, id string) (*models.Issue, er
|
|||||||
func (r *queryResolver) Issues(ctx context.Context, filter *models.IssueFilter) ([]*models.Issue, error) {
|
func (r *queryResolver) Issues(ctx context.Context, filter *models.IssueFilter) ([]*models.Issue, error) {
|
||||||
user := r.Auth.UserFromContext(ctx)
|
user := r.Auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, services.ErrPermissionDenied
|
return nil, xlerrors.PermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
@@ -61,7 +61,7 @@ func (r *queryResolver) Issues(ctx context.Context, filter *models.IssueFilter)
|
|||||||
func (r *queryResolver) Project(ctx context.Context, id string) (*models.Project, error) {
|
func (r *queryResolver) Project(ctx context.Context, id string) (*models.Project, error) {
|
||||||
user := r.Auth.UserFromContext(ctx)
|
user := r.Auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, services.ErrPermissionDenied
|
return nil, xlerrors.PermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
project, err := r.Database.Projects().Find(ctx, id)
|
project, err := r.Database.Projects().Find(ctx, id)
|
||||||
@@ -79,7 +79,7 @@ func (r *queryResolver) Project(ctx context.Context, id string) (*models.Project
|
|||||||
func (r *queryResolver) Projects(ctx context.Context, filter *models.ProjectFilter) ([]*models.Project, error) {
|
func (r *queryResolver) Projects(ctx context.Context, filter *models.ProjectFilter) ([]*models.Project, error) {
|
||||||
user := r.Auth.UserFromContext(ctx)
|
user := r.Auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, services.ErrPermissionDenied
|
return nil, xlerrors.PermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
skipCheck := false
|
skipCheck := false
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
An activity is a measurable activity for a project. It can be measured.
|
||||||
|
"""
|
||||||
|
type Activity {
|
||||||
|
"The activity ID."
|
||||||
|
id: String!
|
||||||
|
"The activity name."
|
||||||
|
name: String!
|
||||||
|
"Whether the activity is countable."
|
||||||
|
countable: Boolean!
|
||||||
|
"Whether the time spent is the unit."
|
||||||
|
unitIsTimeSpent: Boolean!
|
||||||
|
"The name of the unit. If unitIsTime or countable is true, this value should be ignored."
|
||||||
|
unitName: String
|
||||||
|
"The value per unit."
|
||||||
|
unitValue: Float!
|
||||||
|
"The base score value for any performed activity. For uncountables, this is the only points scored."
|
||||||
|
baseValue: Float!
|
||||||
|
|
||||||
|
"Parent project."
|
||||||
|
project: Project!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for the createActivity mutation.
|
||||||
|
"""
|
||||||
|
input ActivityCreateInput {
|
||||||
|
"Project ID to associate it with."
|
||||||
|
projectId: String!
|
||||||
|
"Proejct name."
|
||||||
|
name: String!
|
||||||
|
"The base value of any activity performed. If uncountable, this should be non-zero."
|
||||||
|
baseValue: Float!
|
||||||
|
"Whether the activity is countable. Default: false"
|
||||||
|
countable: Boolean
|
||||||
|
"Whether time spent should be the unit of this activity."
|
||||||
|
unitIsTimeSpent: Boolean
|
||||||
|
"The unit name of the activity."
|
||||||
|
unitName: String
|
||||||
|
"The per-unit value of the activity."
|
||||||
|
unitValue: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input for the editActivity mutation. Not all changes are available as they could create
|
||||||
|
some serious issues with past goals and logs.
|
||||||
|
"""
|
||||||
|
input ActivityEditInput {
|
||||||
|
"The ID of the activity to edit."
|
||||||
|
activityId: String!
|
||||||
|
|
||||||
|
"Update the name of the activity."
|
||||||
|
setName: String
|
||||||
|
"Set the base value of the activity. This has an effect on current and past goals!"
|
||||||
|
setBaseValue: Float
|
||||||
|
"Set the unit name of the activity. This is only for countable, non time spent acitivities."
|
||||||
|
setUnitName: String
|
||||||
|
"Set the unit value of the activity. This has an effect on current and past goals!"
|
||||||
|
setUnitValue: Float
|
||||||
|
}
|
||||||
+23
-14
@@ -1,51 +1,60 @@
|
|||||||
type Issue {
|
type Issue {
|
||||||
|
"The issue ID."
|
||||||
id: String!
|
id: String!
|
||||||
|
"The time at which the issue is created."
|
||||||
createdTime: Time!
|
createdTime: Time!
|
||||||
|
"The time at which the issue was updated."
|
||||||
updatedTime: Time!
|
updatedTime: Time!
|
||||||
|
"Optionally, when this issue is due."
|
||||||
dueTime: Time
|
dueTime: Time
|
||||||
|
"The name of the issue, used in lists."
|
||||||
name: String!
|
name: String!
|
||||||
|
"The issue title. This can be longer than the name."
|
||||||
title: String!
|
title: String!
|
||||||
|
"The description of the issue, in markdown."
|
||||||
description: String!
|
description: String!
|
||||||
|
|
||||||
|
"Parent project."
|
||||||
project: Project
|
project: Project
|
||||||
|
"The issue's owner/creator."
|
||||||
owner: User
|
owner: User
|
||||||
|
"The issue assignee."
|
||||||
assignee: User
|
assignee: User
|
||||||
|
"The issue status."
|
||||||
status: ProjectStatus!
|
status: ProjectStatus!
|
||||||
}
|
}
|
||||||
|
|
||||||
input IssueFilter {
|
input IssueFilter {
|
||||||
"Filter by issue IDs (mostly used internally by data loaders)"
|
"Filter by issue IDs. (mostly used internally by data loaders)"
|
||||||
issueIds: [String!]
|
issueIds: [String!]
|
||||||
"Filter by project IDs"
|
"Filter by project IDs."
|
||||||
projectIds: [String!]
|
projectIds: [String!]
|
||||||
"Filter by owner IDs"
|
"Filter by owner IDs."
|
||||||
ownerIds: [String!]
|
ownerIds: [String!]
|
||||||
"Filter by assignee IDs"
|
"Filter by assignee IDs."
|
||||||
assigneeIds: [String!]
|
assigneeIds: [String!]
|
||||||
"Text search"
|
"Text search."
|
||||||
search: String
|
search: String
|
||||||
"Earliest stage (inclusive)"
|
"Earliest stage. (inclusive)"
|
||||||
minStage: Int
|
minStage: Int
|
||||||
"Latest stage (inclusive)"
|
"Latest stage. (inclusive)"
|
||||||
maxStage: Int
|
maxStage: Int
|
||||||
"Limit the result set"
|
"Limit the result set."
|
||||||
limit: Int
|
limit: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input IssueCreateInput {
|
input IssueCreateInput {
|
||||||
"Project ID"
|
"Project ID."
|
||||||
projectId: String!
|
projectId: String!
|
||||||
"Status stage"
|
"Status name."
|
||||||
statusStage: Int!
|
|
||||||
"Status name"
|
|
||||||
statusName: String!
|
statusName: String!
|
||||||
"A name for the issue."
|
"A name for the issue."
|
||||||
name: String!
|
name: String!
|
||||||
"Description of the issue."
|
"Description of the issue."
|
||||||
description: String!
|
description: String!
|
||||||
"Assign this issue, will default to unassigned"
|
"Assign this issue, will default to unassigned."
|
||||||
assigneeId: String
|
assigneeId: String
|
||||||
"A date when this issue is due"
|
"A date when this issue is due."
|
||||||
dueTime: Time
|
dueTime: Time
|
||||||
"Optional title to use instead of the name when not in a list."
|
"Optional title to use instead of the name when not in a list."
|
||||||
title: String
|
title: String
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
type Mutation {
|
type Mutation {
|
||||||
# PROJECT
|
# PROJECT
|
||||||
"Create a new project"
|
"Create a new project."
|
||||||
createProject(input: ProjectCreateInput!): Project!
|
createProject(input: ProjectCreateInput!): Project!
|
||||||
|
|
||||||
|
# ACTIVITY
|
||||||
|
"Create an activity."
|
||||||
|
createActivity(input: ActivityCreateInput!): Activity!
|
||||||
|
"Edit an activity."
|
||||||
|
editActivity(input: ActivityEditInput!): Activity!
|
||||||
|
|
||||||
# ISSUE
|
# ISSUE
|
||||||
"Create a new issue"
|
"Create a new issue."
|
||||||
createIssue(input: IssueCreateInput!): Issue!
|
createIssue(input: IssueCreateInput!): Issue!
|
||||||
|
|
||||||
# USER
|
# USER
|
||||||
"Log in"
|
"Log in."
|
||||||
loginUser(input: UserLoginInput!): User!
|
loginUser(input: UserLoginInput!): User!
|
||||||
"Log out"
|
"Log out."
|
||||||
logoutUser: User!
|
logoutUser: User!
|
||||||
"Create a new user. This can only be done by administrators."
|
"Create a new user. This can only be done by administrators."
|
||||||
createUser(input: UserCreateInput!): User!
|
createUser(input: UserCreateInput!): User!
|
||||||
"Edit an existing user. This can only be done by administrators"
|
"Edit an existing user. This can only be done by administrators."
|
||||||
editUser(input: UserEditInput!): User!
|
editUser(input: UserEditInput!): User!
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,8 @@ type Project {
|
|||||||
userPermissions: ProjectPermission!
|
userPermissions: ProjectPermission!
|
||||||
"List all project statuses."
|
"List all project statuses."
|
||||||
statuses(filter: ProjectStatusFilter): [ProjectStatus!]!
|
statuses(filter: ProjectStatusFilter): [ProjectStatus!]!
|
||||||
|
"The activities in this project."
|
||||||
|
activities: [Activity!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
"The permissions of a user within the project."
|
"The permissions of a user within the project."
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package generate
|
package generate
|
||||||
|
|
||||||
|
func ActivityID() string {
|
||||||
|
return Generate(16, "A")
|
||||||
|
}
|
||||||
|
|
||||||
func ItemID() string {
|
func ItemID() string {
|
||||||
return Generate(32, "I")
|
return Generate(32, "I")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE activity (
|
||||||
|
activity_id CHAR(16) PRIMARY KEY,
|
||||||
|
project_id CHAR(16) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
countable BOOLEAN NOT NULL,
|
||||||
|
unit_is_time BOOLEAN NOT NULL,
|
||||||
|
unit_name VARCHAR(255) NOT NULL,
|
||||||
|
unit_value FLOAT NOT NULL,
|
||||||
|
base_value FLOAT NOT NULL,
|
||||||
|
|
||||||
|
INDEX(Project_id)
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE activity;
|
||||||
|
-- +goose StatementEnd
|
||||||
+13
-6
@@ -3,10 +3,17 @@ package models
|
|||||||
// Activity is an activity within a project that can be measured and estimated, for example "writing" or "developing".
|
// Activity is an activity within a project that can be measured and estimated, for example "writing" or "developing".
|
||||||
// The points are for the "gamified" aspect of this.
|
// The points are for the "gamified" aspect of this.
|
||||||
type Activity struct {
|
type Activity struct {
|
||||||
ID string `db:"activity_id"`
|
ID string `db:"activity_id"`
|
||||||
ProjectID string `db:"project_id"`
|
ProjectID string `db:"project_id"`
|
||||||
Countable bool `db:"countable"`
|
Name string `db:"name"`
|
||||||
UnitName string `db:"unit_name"`
|
Countable bool `db:"countable"`
|
||||||
UnitValue float64 `db:"unit_value"`
|
UnitIsTimeSpent bool `db:"unit_is_time"`
|
||||||
BaseValue float64 `db:"base_value"`
|
UnitName string `db:"unit_name"`
|
||||||
|
UnitValue float64 `db:"unit_value"`
|
||||||
|
BaseValue float64 `db:"base_value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityFilter struct {
|
||||||
|
ActivityIDs []string
|
||||||
|
ProjectIDs []string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,3 +34,7 @@ func (permission *ProjectPermission) CanManageAnyIssue() bool {
|
|||||||
func (permission *ProjectPermission) CanManagePermissions() bool {
|
func (permission *ProjectPermission) CanManagePermissions() bool {
|
||||||
return permission.Level >= ProjectPermissionLevelOwner
|
return permission.Level >= ProjectPermissionLevelOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (permission *ProjectPermission) CanManageActivities() bool {
|
||||||
|
return permission.Level >= ProjectPermissionLevelAdmin
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user