feat: add notification UI
showPopup was also refactored to be better
This commit is contained in:
		
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -41,19 +41,34 @@ func NewHandler(repo db.Repository) http.Handler {
 | 
			
		||||
 | 
			
		||||
		var subjectID uuid.UUID
 | 
			
		||||
		in := graphql.GetResolverContext(ctx).Args["input"]
 | 
			
		||||
		if typeArg == ObjectTypeProject || typeArg == ObjectTypeTeam {
 | 
			
		||||
			val := reflect.ValueOf(in) // could be any underlying type
 | 
			
		||||
			fieldName := "ProjectID"
 | 
			
		||||
			if typeArg == ObjectTypeTeam {
 | 
			
		||||
				fieldName = "TeamID"
 | 
			
		||||
			}
 | 
			
		||||
			subjectID, ok = val.FieldByName(fieldName).Interface().(uuid.UUID)
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil, errors.New("error while casting subject uuid")
 | 
			
		||||
			}
 | 
			
		||||
		val := reflect.ValueOf(in) // could be any underlying type
 | 
			
		||||
		if val.Kind() == reflect.Ptr {
 | 
			
		||||
			val = reflect.Indirect(val)
 | 
			
		||||
		}
 | 
			
		||||
		var fieldName string
 | 
			
		||||
		switch typeArg {
 | 
			
		||||
		case ObjectTypeTeam:
 | 
			
		||||
			fieldName = "TeamID"
 | 
			
		||||
		case ObjectTypeTask:
 | 
			
		||||
			fieldName = "TaskID"
 | 
			
		||||
		default:
 | 
			
		||||
			fieldName = "ProjectID"
 | 
			
		||||
		}
 | 
			
		||||
		log.WithFields(log.Fields{"typeArg": typeArg, "fieldName": fieldName}).Info("getting field by name")
 | 
			
		||||
		subjectID, ok = val.FieldByName(fieldName).Interface().(uuid.UUID)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return nil, errors.New("error while casting subject uuid")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var err error
 | 
			
		||||
		if level == ActionLevelProject {
 | 
			
		||||
			if typeArg == ObjectTypeTask {
 | 
			
		||||
				log.WithFields(log.Fields{"subjectID": subjectID}).Info("fetching project ID using task ID")
 | 
			
		||||
				subjectID, err = repo.GetProjectIDForTask(ctx, subjectID)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			roles, err := GetProjectRoles(ctx, repo, subjectID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
@@ -151,3 +166,23 @@ func ConvertToRoleCode(r string) RoleCode {
 | 
			
		||||
	}
 | 
			
		||||
	return RoleCodeObserver
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetEntityType converts integer to EntityType enum
 | 
			
		||||
func GetEntityType(entityType int32) EntityType {
 | 
			
		||||
	switch entityType {
 | 
			
		||||
	case 1:
 | 
			
		||||
		return EntityTypeTask
 | 
			
		||||
	default:
 | 
			
		||||
		panic("Not a valid entity type!")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetActionType converts integer to ActionType enum
 | 
			
		||||
func GetActionType(actionType int32) ActionType {
 | 
			
		||||
	switch actionType {
 | 
			
		||||
	case 1:
 | 
			
		||||
		return ActionTypeTaskMemberAdded
 | 
			
		||||
	default:
 | 
			
		||||
		panic("Not a valid entity type!")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -245,6 +245,18 @@ type NewUserAccount struct {
 | 
			
		||||
	RoleCode string `json:"roleCode"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NotificationActor struct {
 | 
			
		||||
	ID   uuid.UUID `json:"id"`
 | 
			
		||||
	Type ActorType `json:"type"`
 | 
			
		||||
	Name string    `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NotificationEntity struct {
 | 
			
		||||
	ID   uuid.UUID  `json:"id"`
 | 
			
		||||
	Type EntityType `json:"type"`
 | 
			
		||||
	Name string     `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OwnedList struct {
 | 
			
		||||
	Teams    []db.Team    `json:"teams"`
 | 
			
		||||
	Projects []db.Project `json:"projects"`
 | 
			
		||||
@@ -470,6 +482,123 @@ func (e ActionLevel) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ActionType string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ActionTypeTaskMemberAdded ActionType = "TASK_MEMBER_ADDED"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AllActionType = []ActionType{
 | 
			
		||||
	ActionTypeTaskMemberAdded,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActionType) IsValid() bool {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case ActionTypeTaskMemberAdded:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActionType) String() string {
 | 
			
		||||
	return string(e)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *ActionType) UnmarshalGQL(v interface{}) error {
 | 
			
		||||
	str, ok := v.(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("enums must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*e = ActionType(str)
 | 
			
		||||
	if !e.IsValid() {
 | 
			
		||||
		return fmt.Errorf("%s is not a valid ActionType", str)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActionType) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ActorType string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ActorTypeUser ActorType = "USER"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AllActorType = []ActorType{
 | 
			
		||||
	ActorTypeUser,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActorType) IsValid() bool {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case ActorTypeUser:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActorType) String() string {
 | 
			
		||||
	return string(e)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *ActorType) UnmarshalGQL(v interface{}) error {
 | 
			
		||||
	str, ok := v.(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("enums must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*e = ActorType(str)
 | 
			
		||||
	if !e.IsValid() {
 | 
			
		||||
		return fmt.Errorf("%s is not a valid ActorType", str)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ActorType) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EntityType string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	EntityTypeTask EntityType = "TASK"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AllEntityType = []EntityType{
 | 
			
		||||
	EntityTypeTask,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e EntityType) IsValid() bool {
 | 
			
		||||
	switch e {
 | 
			
		||||
	case EntityTypeTask:
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e EntityType) String() string {
 | 
			
		||||
	return string(e)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *EntityType) UnmarshalGQL(v interface{}) error {
 | 
			
		||||
	str, ok := v.(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("enums must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*e = EntityType(str)
 | 
			
		||||
	if !e.IsValid() {
 | 
			
		||||
		return fmt.Errorf("%s is not a valid EntityType", str)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e EntityType) MarshalGQL(w io.Writer) {
 | 
			
		||||
	fmt.Fprint(w, strconv.Quote(e.String()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ObjectType string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
 
 | 
			
		||||
@@ -229,6 +229,43 @@ input FindTeam {
 | 
			
		||||
  teamID: UUID!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Query {
 | 
			
		||||
  notifications: [Notification!]!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum EntityType {
 | 
			
		||||
  TASK
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ActorType {
 | 
			
		||||
  USER
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ActionType {
 | 
			
		||||
  TASK_MEMBER_ADDED
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NotificationActor {
 | 
			
		||||
  id: UUID!
 | 
			
		||||
  type: ActorType!
 | 
			
		||||
  name: String!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NotificationEntity {
 | 
			
		||||
  id: UUID!
 | 
			
		||||
  type: EntityType!
 | 
			
		||||
  name: String!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Notification {
 | 
			
		||||
  id: ID!
 | 
			
		||||
  entity: NotificationEntity!
 | 
			
		||||
  actionType: ActionType!
 | 
			
		||||
  actor: NotificationActor!
 | 
			
		||||
  read: Boolean!
 | 
			
		||||
  createdAt: Time!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extend type Mutation {
 | 
			
		||||
  createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
 | 
			
		||||
  deleteProject(input: DeleteProject!):
 | 
			
		||||
@@ -355,9 +392,9 @@ extend type Mutation {
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
 | 
			
		||||
  assignTask(input: AssignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  unassignTask(input: UnassignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTask {
 | 
			
		||||
 
 | 
			
		||||
@@ -269,6 +269,7 @@ func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInpu
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &db.Task{}, err
 | 
			
		||||
	}
 | 
			
		||||
	// r.NotificationQueue.TaskMemberWasAdded(assignedTask.TaskID, userID, assignedTask.UserID)
 | 
			
		||||
	task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
 | 
			
		||||
	return &task, err
 | 
			
		||||
}
 | 
			
		||||
@@ -720,6 +721,61 @@ func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserR
 | 
			
		||||
	return &UpdateUserRolePayload{User: &user}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
 | 
			
		||||
	return obj.NotificationID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *notificationResolver) Entity(ctx context.Context, obj *db.Notification) (*NotificationEntity, error) {
 | 
			
		||||
	log.WithFields(log.Fields{"notificationID": obj.NotificationID}).Info("fetching entity for notification")
 | 
			
		||||
	entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
 | 
			
		||||
	log.WithFields(log.Fields{"entityID": entity.EntityID}).Info("fetched entity")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &NotificationEntity{}, err
 | 
			
		||||
	}
 | 
			
		||||
	entityType := GetEntityType(entity.EntityType)
 | 
			
		||||
	switch entityType {
 | 
			
		||||
	case EntityTypeTask:
 | 
			
		||||
		task, err := r.Repository.GetTaskByID(ctx, entity.EntityID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return &NotificationEntity{}, err
 | 
			
		||||
		}
 | 
			
		||||
		return &NotificationEntity{Type: entityType, ID: entity.EntityID, Name: task.Name}, err
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		panic(fmt.Errorf("not implemented"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *notificationResolver) ActionType(ctx context.Context, obj *db.Notification) (ActionType, error) {
 | 
			
		||||
	entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ActionTypeTaskMemberAdded, err
 | 
			
		||||
	}
 | 
			
		||||
	actionType := GetActionType(entity.ActionType)
 | 
			
		||||
	return actionType, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *notificationResolver) Actor(ctx context.Context, obj *db.Notification) (*NotificationActor, error) {
 | 
			
		||||
	entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &NotificationActor{}, err
 | 
			
		||||
	}
 | 
			
		||||
	log.WithFields(log.Fields{"entityID": entity.ActorID}).Info("fetching actor")
 | 
			
		||||
	user, err := r.Repository.GetUserAccountByID(ctx, entity.ActorID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &NotificationActor{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return &NotificationActor{ID: entity.ActorID, Name: user.FullName, Type: ActorTypeUser}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *notificationResolver) CreatedAt(ctx context.Context, obj *db.Notification) (*time.Time, error) {
 | 
			
		||||
	entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &time.Time{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return &entity.CreatedOn, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *organizationResolver) ID(ctx context.Context, obj *db.Organization) (uuid.UUID, error) {
 | 
			
		||||
	return obj.OrganizationID, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1011,6 +1067,21 @@ func (r *queryResolver) Me(ctx context.Context) (*MePayload, error) {
 | 
			
		||||
	return &MePayload{User: &user, TeamRoles: teamRoles, ProjectRoles: projectRoles}, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *queryResolver) Notifications(ctx context.Context) ([]db.Notification, error) {
 | 
			
		||||
	userID, ok := GetUserID(ctx)
 | 
			
		||||
	log.WithFields(log.Fields{"userID": userID}).Info("fetching notifications")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return []db.Notification{}, errors.New("user id is missing")
 | 
			
		||||
	}
 | 
			
		||||
	notifications, err := r.Repository.GetAllNotificationsForUserID(ctx, userID)
 | 
			
		||||
	if err == sql.ErrNoRows {
 | 
			
		||||
		return []db.Notification{}, nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return []db.Notification{}, err
 | 
			
		||||
	}
 | 
			
		||||
	return notifications, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
 | 
			
		||||
	return obj.TokenID, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1268,6 +1339,9 @@ func (r *Resolver) LabelColor() LabelColorResolver { return &labelColorResolver{
 | 
			
		||||
// Mutation returns MutationResolver implementation.
 | 
			
		||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
 | 
			
		||||
 | 
			
		||||
// Notification returns NotificationResolver implementation.
 | 
			
		||||
func (r *Resolver) Notification() NotificationResolver { return ¬ificationResolver{r} }
 | 
			
		||||
 | 
			
		||||
// Organization returns OrganizationResolver implementation.
 | 
			
		||||
func (r *Resolver) Organization() OrganizationResolver { return &organizationResolver{r} }
 | 
			
		||||
 | 
			
		||||
@@ -1308,6 +1382,7 @@ func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolv
 | 
			
		||||
 | 
			
		||||
type labelColorResolver struct{ *Resolver }
 | 
			
		||||
type mutationResolver struct{ *Resolver }
 | 
			
		||||
type notificationResolver struct{ *Resolver }
 | 
			
		||||
type organizationResolver struct{ *Resolver }
 | 
			
		||||
type projectResolver struct{ *Resolver }
 | 
			
		||||
type projectLabelResolver struct{ *Resolver }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								internal/graph/schema/notification.gql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								internal/graph/schema/notification.gql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
extend type Query {
 | 
			
		||||
  notifications: [Notification!]!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum EntityType {
 | 
			
		||||
  TASK
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ActorType {
 | 
			
		||||
  USER
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ActionType {
 | 
			
		||||
  TASK_MEMBER_ADDED
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NotificationActor {
 | 
			
		||||
  id: UUID!
 | 
			
		||||
  type: ActorType!
 | 
			
		||||
  name: String!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NotificationEntity {
 | 
			
		||||
  id: UUID!
 | 
			
		||||
  type: EntityType!
 | 
			
		||||
  name: String!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Notification {
 | 
			
		||||
  id: ID!
 | 
			
		||||
  entity: NotificationEntity!
 | 
			
		||||
  actionType: ActionType!
 | 
			
		||||
  actor: NotificationActor!
 | 
			
		||||
  read: Boolean!
 | 
			
		||||
  createdAt: Time!
 | 
			
		||||
}
 | 
			
		||||
@@ -16,9 +16,9 @@ extend type Mutation {
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
 | 
			
		||||
  assignTask(input: AssignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
  unassignTask(input: UnassignTaskInput):
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
 | 
			
		||||
    Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input NewTask {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user