feature: UI changes

This commit is contained in:
Jordan Knott 2020-04-20 18:04:27 -05:00
parent c38024e692
commit 7e78ee36b4
45 changed files with 1569 additions and 137 deletions

File diff suppressed because it is too large Load Diff

View File

@ -57,6 +57,12 @@ type NewProject struct {
Name string `json:"name"`
}
type NewProjectLabel struct {
ProjectID uuid.UUID `json:"projectID"`
LabelColorID uuid.UUID `json:"labelColorID"`
Name *string `json:"name"`
}
type NewRefreshToken struct {
UserID string `json:"userId"`
}
@ -100,6 +106,7 @@ type NewUserAccount struct {
type ProfileIcon struct {
URL *string `json:"url"`
Initials *string `json:"initials"`
BgColor *string `json:"bgColor"`
}
type ProjectMember struct {
@ -118,6 +125,11 @@ type RemoveTaskLabelInput struct {
TaskLabelID uuid.UUID `json:"taskLabelID"`
}
type UnassignTaskInput struct {
TaskID uuid.UUID `json:"taskID"`
UserID uuid.UUID `json:"userID"`
}
type UpdateTaskDescriptionInput struct {
TaskID uuid.UUID `json:"taskID"`
Description string `json:"description"`

View File

@ -1,15 +1,25 @@
scalar Time
scalar UUID
type ProjectLabel {
projectLabelID: ID!
createdDate: Time!
colorHex: String!
name: String
}
type TaskLabel {
taskLabelID: ID!
labelColorID: UUID!
projectLabelID: UUID!
assignedDate: Time!
colorHex: String!
name: String
}
type ProfileIcon {
url: String
initials: String
bgColor: String
}
type ProjectMember {
@ -50,6 +60,7 @@ type Project {
owner: ProjectMember!
taskGroups: [TaskGroup!]!
members: [ProjectMember!]!
labels: [ProjectLabel!]!
}
type TaskGroup {
@ -174,6 +185,10 @@ input AssignTaskInput {
userID: UUID!
}
input UnassignTaskInput {
taskID: UUID!
userID: UUID!
}
input UpdateTaskDescriptionInput {
taskID: UUID!
description: String!
@ -189,6 +204,12 @@ input RemoveTaskLabelInput {
taskLabelID: UUID!
}
input NewProjectLabel {
projectID: UUID!
labelColorID: UUID!
name: String
}
type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
@ -197,6 +218,7 @@ type Mutation {
createTeam(input: NewTeam!): Team!
createProject(input: NewProject!): Project!
createProjectLabel(input: NewProjectLabel!): ProjectLabel!
createTaskGroup(input: NewTaskGroup!): TaskGroup!
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
@ -211,6 +233,7 @@ type Mutation {
updateTaskName(input: UpdateTaskName!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
assignTask(input: AssignTaskInput): Task!
unassignTask(input: UnassignTaskInput): Task!
logoutUser(input: LogoutUser!): Boolean!
}

View File

@ -45,6 +45,10 @@ func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject)
return &project, err
}
func (r *mutationResolver) CreateProjectLabel(ctx context.Context, input NewProjectLabel) (*pg.ProjectLabel, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateTaskGroup(ctx context.Context, input NewTaskGroup) (*pg.TaskGroup, error) {
createdAt := time.Now().UTC()
projectID, err := uuid.Parse(input.ProjectID)
@ -164,6 +168,18 @@ func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInpu
return &task, err
}
func (r *mutationResolver) UnassignTask(ctx context.Context, input *UnassignTaskInput) (*pg.Task, error) {
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
if err != nil {
return &pg.Task{}, err
}
_, err = r.Repository.DeleteTaskAssignedByID(ctx, pg.DeleteTaskAssignedByIDParams{input.TaskID, input.UserID})
if err != nil {
return &pg.Task{}, err
}
return &task, nil
}
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
userID, err := uuid.Parse(input.UserID)
if err != nil {
@ -185,7 +201,7 @@ func (r *projectResolver) Owner(ctx context.Context, obj *pg.Project) (*ProjectM
return &ProjectMember{}, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor}
return &ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon}, nil
}
@ -200,11 +216,28 @@ func (r *projectResolver) Members(ctx context.Context, obj *pg.Project) ([]Proje
return members, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor}
members = append(members, ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon})
return members, nil
}
func (r *projectResolver) Labels(ctx context.Context, obj *pg.Project) ([]pg.ProjectLabel, error) {
labels, err := r.Repository.GetProjectLabelsForProject(ctx, obj.ProjectID)
return labels, err
}
func (r *projectLabelResolver) ColorHex(ctx context.Context, obj *pg.ProjectLabel) (string, error) {
labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
if err != nil {
return "", err
}
return labelColor.ColorHex, nil
}
func (r *projectLabelResolver) Name(ctx context.Context, obj *pg.ProjectLabel) (*string, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Users(ctx context.Context) ([]pg.UserAccount, error) {
return r.Repository.GetAllUserAccounts(ctx)
}
@ -306,7 +339,7 @@ func (r *taskResolver) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMem
return taskMembers, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
profileIcon := &ProfileIcon{nil, &initials, &user.ProfileBgColor}
taskMembers = append(taskMembers, ProjectMember{taskMemberLink.UserID, user.FirstName, user.LastName, profileIcon})
}
return taskMembers, nil
@ -326,16 +359,32 @@ func (r *taskGroupResolver) Tasks(ctx context.Context, obj *pg.TaskGroup) ([]pg.
}
func (r *taskLabelResolver) ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) {
labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
projectLabel, err := r.Repository.GetProjectLabelByID(ctx, obj.ProjectLabelID)
if err != nil {
return "", err
}
labelColor, err := r.Repository.GetLabelColorByID(ctx, projectLabel.LabelColorID)
if err != nil {
return "", err
}
return labelColor.ColorHex, nil
}
func (r *taskLabelResolver) Name(ctx context.Context, obj *pg.TaskLabel) (*string, error) {
projectLabel, err := r.Repository.GetProjectLabelByID(ctx, obj.ProjectLabelID)
if err != nil {
return nil, err
}
name := projectLabel.Name
if !name.Valid {
return nil, err
}
return &name.String, err
}
func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *pg.UserAccount) (*ProfileIcon, error) {
initials := string([]rune(obj.FirstName)[0]) + string([]rune(obj.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
profileIcon := &ProfileIcon{nil, &initials, &obj.ProfileBgColor}
return profileIcon, nil
}
@ -345,6 +394,9 @@ func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Project returns ProjectResolver implementation.
func (r *Resolver) Project() ProjectResolver { return &projectResolver{r} }
// ProjectLabel returns ProjectLabelResolver implementation.
func (r *Resolver) ProjectLabel() ProjectLabelResolver { return &projectLabelResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
@ -362,6 +414,7 @@ func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolv
type mutationResolver struct{ *Resolver }
type projectResolver struct{ *Resolver }
type projectLabelResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type taskGroupResolver struct{ *Resolver }
@ -374,6 +427,9 @@ type userAccountResolver struct{ *Resolver }
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
func (r *taskLabelResolver) ProjectLabelID(ctx context.Context, obj *pg.TaskLabel) (uuid.UUID, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *userAccountResolver) DisplayName(ctx context.Context, obj *pg.UserAccount) (string, error) {
return obj.FirstName + " " + obj.LastName, nil
}

View File

@ -0,0 +1,7 @@
CREATE TABLE project_label (
project_label_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id uuid NOT NULL REFERENCES project(project_id),
label_color_id uuid NOT NULL REFERENCES label_color(label_color_id),
created_date timestamptz NOT NULL,
name text
);

View File

@ -1,6 +1,6 @@
CREATE TABLE task_label (
task_label_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id uuid NOT NULL REFERENCES task(task_id),
label_color_id uuid NOT NULL REFERENCES label_color(label_color_id),
project_label_id uuid NOT NULL REFERENCES project_label(project_label_id),
assigned_date timestamptz NOT NULL
);

View File

@ -0,0 +1 @@
ALTER TABLE user_account ADD COLUMN profile_bg_color text NOT NULL DEFAULT '#7367F0';

View File

@ -29,6 +29,14 @@ type Project struct {
Owner uuid.UUID `json:"owner"`
}
type ProjectLabel struct {
ProjectLabelID uuid.UUID `json:"project_label_id"`
ProjectID uuid.UUID `json:"project_id"`
LabelColorID uuid.UUID `json:"label_color_id"`
CreatedDate time.Time `json:"created_date"`
Name sql.NullString `json:"name"`
}
type RefreshToken struct {
TokenID uuid.UUID `json:"token_id"`
UserID uuid.UUID `json:"user_id"`
@ -64,7 +72,7 @@ type TaskGroup struct {
type TaskLabel struct {
TaskLabelID uuid.UUID `json:"task_label_id"`
TaskID uuid.UUID `json:"task_id"`
LabelColorID uuid.UUID `json:"label_color_id"`
ProjectLabelID uuid.UUID `json:"project_label_id"`
AssignedDate time.Time `json:"assigned_date"`
}
@ -83,4 +91,5 @@ type UserAccount struct {
Email string `json:"email"`
Username string `json:"username"`
PasswordHash string `json:"password_hash"`
ProfileBgColor string `json:"profile_bg_color"`
}

View File

@ -22,6 +22,10 @@ type Repository interface {
GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error)
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error
@ -55,6 +59,7 @@ type Repository interface {
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error)
}
type repoSvc struct {

View File

@ -0,0 +1,92 @@
// Code generated by sqlc. DO NOT EDIT.
// source: project_label.sql
package pg
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
)
const createProjectLabel = `-- name: CreateProjectLabel :one
INSERT INTO project_label (project_id, label_color_id, created_date, name)
VALUES ($1, $2, $3, $4) RETURNING project_label_id, project_id, label_color_id, created_date, name
`
type CreateProjectLabelParams struct {
ProjectID uuid.UUID `json:"project_id"`
LabelColorID uuid.UUID `json:"label_color_id"`
CreatedDate time.Time `json:"created_date"`
Name sql.NullString `json:"name"`
}
func (q *Queries) CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error) {
row := q.db.QueryRowContext(ctx, createProjectLabel,
arg.ProjectID,
arg.LabelColorID,
arg.CreatedDate,
arg.Name,
)
var i ProjectLabel
err := row.Scan(
&i.ProjectLabelID,
&i.ProjectID,
&i.LabelColorID,
&i.CreatedDate,
&i.Name,
)
return i, err
}
const getProjectLabelByID = `-- name: GetProjectLabelByID :one
SELECT project_label_id, project_id, label_color_id, created_date, name FROM project_label WHERE project_label_id = $1
`
func (q *Queries) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) {
row := q.db.QueryRowContext(ctx, getProjectLabelByID, projectLabelID)
var i ProjectLabel
err := row.Scan(
&i.ProjectLabelID,
&i.ProjectID,
&i.LabelColorID,
&i.CreatedDate,
&i.Name,
)
return i, err
}
const getProjectLabelsForProject = `-- name: GetProjectLabelsForProject :many
SELECT project_label_id, project_id, label_color_id, created_date, name FROM project_label WHERE project_id = $1
`
func (q *Queries) GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error) {
rows, err := q.db.QueryContext(ctx, getProjectLabelsForProject, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ProjectLabel
for rows.Next() {
var i ProjectLabel
if err := rows.Scan(
&i.ProjectLabelID,
&i.ProjectID,
&i.LabelColorID,
&i.CreatedDate,
&i.Name,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -11,6 +11,7 @@ import (
type Querier interface {
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error)
CreateTaskAssigned(ctx context.Context, arg CreateTaskAssignedParams) (TaskAssigned, error)
@ -21,6 +22,7 @@ type Querier interface {
DeleteExpiredTokens(ctx context.Context) error
DeleteRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) error
DeleteRefreshTokenByUserID(ctx context.Context, userID uuid.UUID) error
DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, 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)
@ -35,6 +37,8 @@ type Querier interface {
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
GetRefreshTokenByID(ctx context.Context, tokenID uuid.UUID) (RefreshToken, error)
GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, error)
GetTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (TaskGroup, error)

View File

@ -33,6 +33,27 @@ func (q *Queries) CreateTaskAssigned(ctx context.Context, arg CreateTaskAssigned
return i, err
}
const deleteTaskAssignedByID = `-- name: DeleteTaskAssignedByID :one
DELETE FROM task_assigned WHERE task_id = $1 AND user_id = $2 RETURNING task_assigned_id, task_id, user_id, assigned_date
`
type DeleteTaskAssignedByIDParams struct {
TaskID uuid.UUID `json:"task_id"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) DeleteTaskAssignedByID(ctx context.Context, arg DeleteTaskAssignedByIDParams) (TaskAssigned, error) {
row := q.db.QueryRowContext(ctx, deleteTaskAssignedByID, arg.TaskID, arg.UserID)
var i TaskAssigned
err := row.Scan(
&i.TaskAssignedID,
&i.TaskID,
&i.UserID,
&i.AssignedDate,
)
return i, err
}
const getAssignedMembersForTask = `-- name: GetAssignedMembersForTask :many
SELECT task_assigned_id, task_id, user_id, assigned_date FROM task_assigned WHERE task_id = $1
`

View File

@ -11,30 +11,30 @@ import (
)
const createTaskLabelForTask = `-- name: CreateTaskLabelForTask :one
INSERT INTO task_label (task_id, label_color_id, assigned_date)
VALUES ($1, $2, $3) RETURNING task_label_id, task_id, label_color_id, assigned_date
INSERT INTO task_label (task_id, project_label_id, assigned_date)
VALUES ($1, $2, $3) RETURNING task_label_id, task_id, project_label_id, assigned_date
`
type CreateTaskLabelForTaskParams struct {
TaskID uuid.UUID `json:"task_id"`
LabelColorID uuid.UUID `json:"label_color_id"`
ProjectLabelID uuid.UUID `json:"project_label_id"`
AssignedDate time.Time `json:"assigned_date"`
}
func (q *Queries) CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error) {
row := q.db.QueryRowContext(ctx, createTaskLabelForTask, arg.TaskID, arg.LabelColorID, arg.AssignedDate)
row := q.db.QueryRowContext(ctx, createTaskLabelForTask, arg.TaskID, arg.ProjectLabelID, arg.AssignedDate)
var i TaskLabel
err := row.Scan(
&i.TaskLabelID,
&i.TaskID,
&i.LabelColorID,
&i.ProjectLabelID,
&i.AssignedDate,
)
return i, err
}
const getTaskLabelsForTaskID = `-- name: GetTaskLabelsForTaskID :many
SELECT task_label_id, task_id, label_color_id, assigned_date FROM task_label WHERE task_id = $1
SELECT task_label_id, task_id, project_label_id, assigned_date FROM task_label WHERE task_id = $1
`
func (q *Queries) GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error) {
@ -49,7 +49,7 @@ func (q *Queries) GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID)
if err := rows.Scan(
&i.TaskLabelID,
&i.TaskID,
&i.LabelColorID,
&i.ProjectLabelID,
&i.AssignedDate,
); err != nil {
return nil, err

View File

@ -13,7 +13,7 @@ import (
const createUserAccount = `-- name: CreateUserAccount :one
INSERT INTO user_account(first_name, last_name, email, username, created_at, password_hash)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING user_id, created_at, first_name, last_name, email, username, password_hash
RETURNING user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color
`
type CreateUserAccountParams struct {
@ -43,12 +43,13 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
)
return i, err
}
const getAllUserAccounts = `-- name: GetAllUserAccounts :many
SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account
SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account
`
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
@ -68,6 +69,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
); err != nil {
return nil, err
}
@ -83,7 +85,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
}
const getUserAccountByID = `-- name: GetUserAccountByID :one
SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account WHERE user_id = $1
SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account WHERE user_id = $1
`
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
@ -97,12 +99,13 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
)
return i, err
}
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
SELECT user_id, created_at, first_name, last_name, email, username, password_hash FROM user_account WHERE username = $1
SELECT user_id, created_at, first_name, last_name, email, username, password_hash, profile_bg_color FROM user_account WHERE username = $1
`
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
@ -116,6 +119,7 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
)
return i, err
}

View File

@ -0,0 +1,9 @@
-- name: CreateProjectLabel :one
INSERT INTO project_label (project_id, label_color_id, created_date, name)
VALUES ($1, $2, $3, $4) RETURNING *;
-- name: GetProjectLabelsForProject :many
SELECT * FROM project_label WHERE project_id = $1;
-- name: GetProjectLabelByID :one
SELECT * FROM project_label WHERE project_label_id = $1;

View File

@ -4,3 +4,6 @@ INSERT INTO task_assigned (task_id, user_id, assigned_date)
-- name: GetAssignedMembersForTask :many
SELECT * FROM task_assigned WHERE task_id = $1;
-- name: DeleteTaskAssignedByID :one
DELETE FROM task_assigned WHERE task_id = $1 AND user_id = $2 RETURNING *;

View File

@ -1,5 +1,5 @@
-- name: CreateTaskLabelForTask :one
INSERT INTO task_label (task_id, label_color_id, assigned_date)
INSERT INTO task_label (task_id, project_label_id, assigned_date)
VALUES ($1, $2, $3) RETURNING *;
-- name: GetTaskLabelsForTaskID :many

View File

@ -41,13 +41,27 @@ const GlobalTopNavbar: React.FC = () => {
return (
<>
<TopNavbar
bgColor={data ? data.me.profileIcon.bgColor ?? '#7367F0' : '#7367F0'}
firstName={data ? data.me.firstName : ''}
lastName={data ? data.me.lastName : ''}
initials={!data ? '' : data.me.profileIcon.initials ?? ''}
onNotificationClick={() => console.log('beep')}
onProfileClick={onProfileClick}
/>
{menu.isOpen && <DropdownMenu onLogout={onLogout} left={menu.left} top={menu.top} />}
{menu.isOpen && (
<DropdownMenu
onCloseDropdown={() => {
setMenu({
top: 0,
left: 0,
isOpen: false,
});
}}
onLogout={onLogout}
left={menu.left}
top={menu.top}
/>
)}
</>
);
};

View File

@ -4,7 +4,7 @@ import TaskDetails from 'shared/components/TaskDetails';
import PopupMenu from 'shared/components/PopupMenu';
import MemberManager from 'shared/components/MemberManager';
import { useRouteMatch, useHistory } from 'react-router';
import { useFindTaskQuery, useAssignTaskMutation } from 'shared/generated/graphql';
import { useFindTaskQuery, useAssignTaskMutation, useUnassignTaskMutation } from 'shared/generated/graphql';
import UserIDContext from 'App/context';
type DetailsProps = {
@ -15,6 +15,7 @@ type DetailsProps = {
onDeleteTask: (task: Task) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
availableMembers: Array<TaskUser>;
refreshCache: () => void;
};
const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
@ -27,13 +28,25 @@ const Details: React.FC<DetailsProps> = ({
onDeleteTask,
onOpenAddLabelPopup,
availableMembers,
refreshCache,
}) => {
const { userID } = useContext(UserIDContext);
const history = useHistory();
const match = useRouteMatch();
const [memberPopupData, setMemberPopupData] = useState(initialMemberPopupState);
const { loading, data } = useFindTaskQuery({ variables: { taskID } });
const [assignTask] = useAssignTaskMutation();
const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
const [assignTask] = useAssignTaskMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
const [unassignTask] = useUnassignTaskMutation({
onCompleted: () => {
refetch();
refreshCache();
},
});
if (loading) {
return <div>loading</div>;
}
@ -47,6 +60,7 @@ const Details: React.FC<DetailsProps> = ({
profileIcon: {
url: null,
initials: assigned.profileIcon.initials ?? null,
bgColor: assigned.profileIcon.bgColor ?? null,
},
};
});
@ -93,10 +107,13 @@ const Details: React.FC<DetailsProps> = ({
>
<MemberManager
availableMembers={availableMembers}
activeMembers={[]}
activeMembers={taskMembers}
onMemberChange={(member, isActive) => {
console.log(`is active ${member.userID} - ${isActive}`);
if (isActive) {
assignTask({ variables: { taskID: data.findTask.taskID, userID: userID ?? '' } });
} else {
unassignTask({ variables: { taskID: data.findTask.taskID, userID: userID ?? '' } });
}
console.log(member, isActive);
}}

View File

@ -1,5 +1,6 @@
import styled from 'styled-components';
export const Board = styled.div`
margin-left: 36px;
margin-top: 12px;
margin-left: 8px;
`;

View File

@ -109,9 +109,10 @@ const Project = () => {
);
},
});
const { loading, data } = useFindProjectQuery({
const { loading, data, refetch } = useFindProjectQuery({
variables: { projectId },
onCompleted: newData => {
console.log('beep!');
const newListsData: BoardState = { tasks: {}, columns: {} };
newData.findProject.taskGroups.forEach(taskGroup => {
newListsData.columns[taskGroup.taskGroupID] = {
@ -121,15 +122,27 @@ const Project = () => {
tasks: [],
};
taskGroup.tasks.forEach(task => {
const taskMembers = task.assigned.map(assigned => {
return {
userID: assigned.userID,
displayName: `${assigned.firstName} ${assigned.lastName}`,
profileIcon: {
url: null,
initials: assigned.profileIcon.initials ?? '',
bgColor: assigned.profileIcon.bgColor ?? '#7367F0',
},
};
});
newListsData.tasks[task.taskID] = {
taskID: task.taskID,
taskGroup: {
taskGroupID: taskGroup.taskGroupID,
},
name: task.name,
position: task.position,
labels: [],
position: task.position,
description: task.description ?? undefined,
members: taskMembers,
};
});
});
@ -196,15 +209,16 @@ const Project = () => {
const availableMembers = data.findProject.members.map(member => {
return {
displayName: `${member.firstName} ${member.lastName}`,
profileIcon: { url: null, initials: member.profileIcon.initials ?? null },
profileIcon: {
url: null,
initials: member.profileIcon.initials ?? null,
bgColor: member.profileIcon.bgColor ?? null,
},
userID: member.userID,
};
});
return (
<>
<TitleWrapper>
<Title>{data.findProject.name}</Title>
</TitleWrapper>
<KanbanBoard
listsData={listsData}
onCardDrop={onCardDrop}
@ -253,6 +267,10 @@ const Project = () => {
path={`${match.path}/c/:taskID`}
render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
<Details
refreshCache={() => {
console.log('beep 2!');
refetch();
}}
availableMembers={availableMembers}
projectURL={match.url}
taskID={routeProps.match.params.taskID}

View File

@ -37,6 +37,7 @@ type InnerTaskGroup = {
type ProfileIcon = {
url: string | null;
initials: string | null;
bgColor: string | null;
};
type TaskUser = {

View File

@ -98,9 +98,6 @@ export const ListCardOperation = styled.span`
display: flex;
align-content: center;
justify-content: center;
background-color: ${props => mixin.darken('#262c49', 0.15)};
background-clip: padding-box;
background-origin: padding-box;
border-radius: 3px;
opacity: 0.8;
padding: 6px;
@ -108,6 +105,10 @@ export const ListCardOperation = styled.span`
right: 2px;
top: 2px;
z-index: 10;
&:hover {
background-color: ${props => mixin.darken('#262c49', 0.45)};
}
`;
export const CardTitle = styled.span`
@ -120,3 +121,34 @@ export const CardTitle = styled.span`
word-wrap: break-word;
color: #c2c6dc;
`;
export const CardMembers = styled.div`
float: right;
margin: 0 -2px 0 0;
`;
export const CardMember = styled.div<{ bgColor: string }>`
height: 28px;
width: 28px;
float: right;
margin: 0 0 4px 4px;
background-color: ${props => props.bgColor};
color: #fff;
border-radius: 25em;
cursor: pointer;
display: block;
overflow: visible;
position: relative;
text-decoration: none;
z-index: 0;
`;
export const CardMemberInitials = styled.div`
height: 28px;
width: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
`;

View File

@ -18,6 +18,9 @@ import {
ListCardLabel,
ListCardOperation,
CardTitle,
CardMembers,
CardMember,
CardMemberInitials,
} from './Styles';
type DueDate = {
@ -42,6 +45,7 @@ type Props = {
watched?: boolean;
labels?: Label[];
wrapperProps?: any;
members?: Array<TaskUser> | null;
};
const Card = React.forwardRef(
@ -58,6 +62,7 @@ const Card = React.forwardRef(
description,
checklists,
watched,
members,
}: Props,
$cardRef: any,
) => {
@ -95,9 +100,11 @@ const Card = React.forwardRef(
{...wrapperProps}
>
<ListCardInnerContainer ref={$innerCardRef}>
{isActive && (
<ListCardOperation>
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
</ListCardOperation>
)}
<ListCardDetails>
<ListCardLabels>
{labels &&
@ -132,6 +139,14 @@ const Card = React.forwardRef(
</ListCardBadge>
)}
</ListCardBadges>
<CardMembers>
{members &&
members.map(member => (
<CardMember key={member.userID} bgColor={member.profileIcon.bgColor ?? '#7367F0'}>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
))}
</CardMembers>
</ListCardDetails>
</ListCardInnerContainer>
</ListCardContainer>

View File

@ -1,8 +1,7 @@
import React, { createRef, useState } from 'react';
import styled from 'styled-components';
import DropdownMenu from '.';
import { action } from '@storybook/addon-actions';
import DropdownMenu from '.';
export default {
component: DropdownMenu,
@ -50,7 +49,16 @@ export const Default = () => {
Click me
</Button>
</Container>
{menu.isOpen && <DropdownMenu onLogout={action('on logout')} left={menu.left} top={menu.top} />}
{menu.isOpen && (
<DropdownMenu
onCloseDropdown={() => {
setMenu({ top: 0, left: 0, isOpen: false });
}}
onLogout={action('on logout')}
left={menu.left}
top={menu.top}
/>
)}
</>
);
};

View File

@ -1,5 +1,5 @@
import React from 'react';
import React, { useRef } from 'react';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Exit, User } from 'shared/icons';
import { Separator, Container, WrapperDiamond, Wrapper, ActionsList, ActionItem, ActionTitle } from './Styles';
@ -7,11 +7,14 @@ type DropdownMenuProps = {
left: number;
top: number;
onLogout: () => void;
onCloseDropdown: () => void;
};
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout }) => {
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onCloseDropdown }) => {
const $containerRef = useRef<HTMLDivElement>(null);
useOnOutsideClick($containerRef, true, onCloseDropdown, null);
return (
<Container left={left} top={top}>
<Container ref={$containerRef} left={left} top={top}>
<Wrapper>
<ActionItem>
<User size={16} color="#c2c6dc" />

View File

@ -23,7 +23,9 @@ export const Default = () => {
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description: 'hello!',
members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }],
members: [
{ userID: '1', profileIcon: { url: null, initials: null, bgColor: null }, displayName: 'Jordan Knott' },
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}

View File

@ -159,6 +159,7 @@ const Lists: React.FC<Props> = ({
description=""
title={task.name}
labels={task.labels}
members={task.members}
onClick={() => onCardClick(task)}
onContextMenu={onQuickEditorOpen}
/>

View File

@ -0,0 +1,50 @@
import styled from 'styled-components';
export const Profile = styled.div`
margin: 8px 0;
min-height: 56px;
position: relative;
`;
export const ProfileIcon = styled.div<{ bgColor: string }>`
float: left;
margin: 2px;
background-color: ${props => props.bgColor};
border-radius: 25em;
display: block;
height: 50px;
overflow: hidden;
position: relative;
width: 50px;
z-index: 1;
`;
export const ProfileInfo = styled.div`
margin: 0 0 0 64px;
word-wrap: break-word;
`;
export const InfoTitle = styled.h3`
margin: 0 40px 0 0;
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: #172b4d;
`;
export const InfoUsername = styled.p`
color: #5e6c84;
font-size: 14px;
line-height: 20px;
`;
export const InfoBio = styled.p`
font-size: 14px;
line-height: 20px;
color: #5e6c84;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
padding: 0;
`;

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Profile, ProfileIcon, ProfileInfo, InfoTitle, InfoUsername, InfoBio } from './Styles';
type MiniProfileProps = {
displayName: string;
username: string;
bio: string;
profileIcon: ProfileIcon;
};
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon }) => {
return (
<>
<Profile>
<ProfileIcon bgColor={profileIcon.bgColor ?? ''}>{profileIcon.initials}</ProfileIcon>
<ProfileInfo>
<InfoTitle>{displayName}</InfoTitle>
<InfoUsername>{username}</InfoUsername>
<InfoBio>{bio}</InfoBio>
</ProfileInfo>
</Profile>
</>
);
};
export default MiniProfile;

View File

@ -17,13 +17,12 @@ export const ClickableOverlay = styled.div`
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
padding: 50px;
`;
export const StyledModal = styled.div<{ width: number }>`
display: inline-block;
position: relative;
margin: 48px 0 80px;
width: 100%;
background: #262c49;
max-width: ${props => props.width}px;

View File

@ -1,32 +1,14 @@
import styled, { css } from 'styled-components';
export const LogoWrapper = styled.div`
margin: 20px 0px 20px;
position: relative;
width: 100%;
height: 42px;
line-height: 42px;
padding-left: 64px;
color: rgb(222, 235, 255);
cursor: pointer;
user-select: none;
transition: color 0.1s ease 0s;
`;
export const Logo = styled.div`
position: absolute;
left: 19px;
`;
export const Logo = styled.div``;
export const LogoTitle = styled.div`
position: relative;
right: 12px;
position: absolute;
visibility: hidden;
opacity: 0;
font-size: 24px;
font-weight: 600;
transition: right 0.1s ease 0s, visibility, opacity, transform 0.25s ease;
transition: visibility, opacity, transform 0.25s ease;
color: #7367f0;
`;
export const ActionContainer = styled.div`
@ -54,6 +36,10 @@ export const IconWrapper = styled.div`
export const ActionButtonContainer = styled.div`
padding: 0 12px;
position: relative;
& > a:first-child > div {
padding-top: 48px;
}
`;
export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
@ -65,7 +51,7 @@ export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
`}
border-radius: 6px;
cursor: pointer;
padding: 10px 15px;
padding: 24px 15px;
display: flex;
align-items: center;
&:hover ${ActionButtonTitle} {
@ -76,6 +62,20 @@ export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
}
`;
export const LogoWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: relative;
width: 100%;
height: 80px;
color: rgb(222, 235, 255);
cursor: pointer;
transition: color 0.1s ease 0s, border 0.1s ease 0s;
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
`;
export const Container = styled.aside`
z-index: 100;
position: fixed;
@ -87,13 +87,15 @@ export const Container = styled.aside`
transform: translateZ(0px);
background: #10163a;
transition: all 0.1s ease 0s;
border-right: 1px solid rgba(65, 69, 97, 0.65);
&:hover {
width: 260px;
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px;
border-right: 1px solid rgba(65, 69, 97, 0);
}
&:hover ${LogoTitle} {
right: 0px;
bottom: -12px;
visibility: visible;
opacity: 1;
}
@ -102,4 +104,8 @@ export const Container = styled.aside`
visibility: visible;
opacity: 1;
}
&:hover ${LogoWrapper} {
border-bottom: 1px solid rgba(65, 69, 97, 0);
}
`;

View File

@ -35,9 +35,7 @@ export const ButtonContainer: React.FC = ({ children }) => (
export const PrimaryLogo = () => {
return (
<LogoWrapper>
<Logo>
<Citadel size={42} />
</Logo>
<LogoTitle>Citadel</LogoTitle>
</LogoWrapper>
);

View File

@ -6,6 +6,7 @@ import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import DueDateManager from 'shared/components/DueDateManager';
import MiniProfile from 'shared/components/MiniProfile';
import PopupMenu from '.';
import NormalizeStyles from 'App/NormalizeStyles';
@ -115,7 +116,7 @@ export const MemberManagerPopup = () => {
<PopupMenu title="Members" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<MemberManager
availableMembers={[
{ userID: '1', displayName: 'Jordan Knott', profileIcon: { url: null, initials: null } },
{ userID: '1', displayName: 'Jordan Knott', profileIcon: { bgColor: null, url: null, initials: null } },
]}
activeMembers={[]}
onMemberChange={action('member change')}
@ -158,7 +159,9 @@ export const DueDateManagerPopup = () => {
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description: 'hello!',
members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }],
members: [
{ userID: '1', profileIcon: { bgColor: null, url: null, initials: null }, displayName: 'Jordan Knott' },
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
@ -189,3 +192,47 @@ export const DueDateManagerPopup = () => {
</>
);
};
export const MiniProfilePopup = () => {
const $buttonRef = useRef<HTMLButtonElement>(null);
const [popupData, setPopupData] = useState(initalState);
return (
<>
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<MiniProfile
displayName="Jordan Knott"
profileIcon={{ url: null, bgColor: '#000', initials: 'JK' }}
username="@jordanthedev"
bio="Stuff and things"
/>
</PopupMenu>
)}
<span
style={{
width: '60px',
textAlign: 'center',
margin: '25px auto',
cursor: 'pointer',
color: '#fff',
background: '#f00',
}}
ref={$buttonRef}
onClick={() => {
if ($buttonRef && $buttonRef.current) {
const pos = $buttonRef.current.getBoundingClientRect();
setPopupData({
isOpen: true,
left: pos.left,
top: pos.top + pos.height + 10,
});
}
}}
>
Open
</span>
</>
);
};

View File

@ -281,3 +281,9 @@ export const NoDueDateLabel = styled.span`
font-size: 14px;
cursor: pointer;
`;
export const UnassignedLabel = styled.div`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
`;

View File

@ -35,7 +35,13 @@ export const Default = () => {
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
description,
members: [{ userID: '1', profileIcon: { url: null, initials: null }, displayName: 'Jordan Knott' }],
members: [
{
userID: '1',
profileIcon: { bgColor: null, url: null, initials: null },
displayName: 'Jordan Knott',
},
],
}}
onTaskNameChange={action('task name change')}
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}

View File

@ -4,6 +4,7 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import {
NoDueDateLabel,
UnassignedLabel,
TaskDetailsAddMember,
TaskGroupLabel,
TaskGroupLabelName,
@ -126,11 +127,16 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onTaskNameChange(task, taskName);
}
};
const $unassignedRef = useRef<HTMLDivElement>(null);
const $addMemberRef = useRef<HTMLDivElement>(null);
const onUnassignedClick = () => {
const bounds = convertDivElementRefToBounds($unassignedRef);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
};
const onAddMember = () => {
console.log('beep!');
const bounds = convertDivElementRefToBounds($addMemberRef);
console.log(bounds);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
@ -191,6 +197,12 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<TaskDetailsSidebar>
<TaskDetailSectionTitle>Assignees</TaskDetailSectionTitle>
<TaskDetailAssignees>
{task.members && task.members.length === 0 ? (
<UnassignedLabel ref={$unassignedRef} onClick={onUnassignedClick}>
Unassigned
</UnassignedLabel>
) : (
<>
{task.members &&
task.members.map(member => {
console.log(member);
@ -205,6 +217,8 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<Plus size={16} color="#c2c6dc" />
</TaskDetailsAddMemberIcon>
</TaskDetailsAddMember>
</>
)}
</TaskDetailAssignees>
<TaskDetailSectionTitle>Labels</TaskDetailSectionTitle>
<TaskDetailLabels>

View File

@ -1,19 +1,18 @@
import styled from 'styled-components';
export const NavbarWrapper = styled.div`
height: 103px;
padding: 1.3rem 2.2rem 2.2rem;
width: 100%;
`;
export const NavbarHeader = styled.header`
border-radius: 0.5rem;
padding: 0.8rem 1rem;
height: 80px;
padding: 0 1.75rem;
display: flex;
align-items: center;
justify-content: space-between;
background: rgb(16, 22, 58);
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
`;
export const Breadcrumbs = styled.div`
color: rgb(94, 108, 132);
@ -26,7 +25,14 @@ export const BreadcrumpSeparator = styled.span`
margin: 0px 10px;
`;
export const ProjectActions = styled.div``;
export const ProjectActions = styled.div`
align-items: flex-start;
display: flex;
flex: 1;
flex-direction: column;
min-width: 1px;
`;
export const GlobalActions = styled.div`
display: flex;
align-items: center;
@ -55,7 +61,7 @@ export const ProfileNameSecondary = styled.small`
color: #c2c6dc;
`;
export const ProfileIcon = styled.div`
export const ProfileIcon = styled.div<{ bgColor: string }>`
margin-left: 10px;
width: 40px;
height: 40px;
@ -65,6 +71,48 @@ export const ProfileIcon = styled.div`
justify-content: center;
color: #fff;
font-weight: 700;
background: rgb(115, 103, 240);
background: ${props => props.bgColor};
cursor: pointer;
`;
export const ProjectMeta = styled.div`
align-items: center;
display: flex;
max-width: 100%;
min-height: 51px;
`;
export const ProjectTabs = styled.div`
align-items: flex-end;
align-self: stretch;
display: flex;
flex: 1 0 auto;
justify-content: flex-start;
max-width: 100%;
`;
export const ProjectTab = styled.span`
font-size: 80%;
color: #c2c6dc;
font-size: 15px;
cursor: default;
display: flex;
line-height: normal;
min-width: 1px;
transition-duration: 0.2s;
transition-property: box-shadow, color;
white-space: nowrap;
flex: 0 1 auto;
padding-bottom: 12px;
box-shadow: inset 0 -2px #d85dd8;
color: #d85dd8;
`;
export const ProjectName = styled.h1`
color: #c2c6dc;
margin-top: 9px;
font-weight: 600;
font-size: 20px;
`;

View File

@ -38,13 +38,23 @@ export const Default = () => {
<NormalizeStyles />
<BaseStyles />
<TopNavbar
bgColor="#7367F0"
firstName="Jordan"
lastName="Knott"
initials="JK"
onNotificationClick={action('notifications click')}
onProfileClick={onClick}
/>
{menu.isOpen && <DropdownMenu onLogout={action('on logout')} left={menu.left} top={menu.top} />}
{menu.isOpen && (
<DropdownMenu
onCloseDropdown={() => {
setMenu({ left: 0, top: 0, isOpen: false });
}}
onLogout={action('on logout')}
left={menu.left}
top={menu.top}
/>
)}
</>
);
};

View File

@ -5,6 +5,10 @@ import {
NotificationContainer,
GlobalActions,
ProjectActions,
ProjectMeta,
ProjectName,
ProjectTabs,
ProjectTab,
NavbarWrapper,
NavbarHeader,
Breadcrumbs,
@ -19,11 +23,19 @@ import {
type NavBarProps = {
onProfileClick: (bottom: number, right: number) => void;
onNotificationClick: () => void;
bgColor: string;
firstName: string;
lastName: string;
initials: string;
};
const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick, firstName, lastName, initials }) => {
const NavBar: React.FC<NavBarProps> = ({
onProfileClick,
onNotificationClick,
firstName,
lastName,
initials,
bgColor,
}) => {
const $profileRef: any = useRef(null);
const handleProfileClick = () => {
console.log('click');
@ -34,13 +46,12 @@ const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick, fi
<NavbarWrapper>
<NavbarHeader>
<ProjectActions>
<Breadcrumbs>
Projects
<BreadcrumpSeparator>/</BreadcrumpSeparator>
project name
<BreadcrumpSeparator>/</BreadcrumpSeparator>
Board
</Breadcrumbs>
<ProjectMeta>
<ProjectName>Production Team</ProjectName>
</ProjectMeta>
<ProjectTabs>
<ProjectTab>Board</ProjectTab>
</ProjectTabs>
</ProjectActions>
<GlobalActions>
<NotificationContainer onClick={onNotificationClick}>
@ -53,7 +64,7 @@ const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick, fi
</ProfileNamePrimary>
<ProfileNameSecondary>Manager</ProfileNameSecondary>
</ProfileNameWrapper>
<ProfileIcon ref={$profileRef} onClick={handleProfileClick}>
<ProfileIcon ref={$profileRef} onClick={handleProfileClick} bgColor={bgColor}>
{initials}
</ProfileIcon>
</ProfileContainer>

View File

@ -15,17 +15,28 @@ export type Scalars = {
export type ProjectLabel = {
__typename?: 'ProjectLabel';
projectLabelID: Scalars['ID'];
createdDate: Scalars['Time'];
colorHex: Scalars['String'];
name?: Maybe<Scalars['String']>;
};
export type TaskLabel = {
__typename?: 'TaskLabel';
taskLabelID: Scalars['ID'];
labelColorID: Scalars['UUID'];
projectLabelID: Scalars['UUID'];
assignedDate: Scalars['Time'];
colorHex: Scalars['String'];
name?: Maybe<Scalars['String']>;
};
export type ProfileIcon = {
__typename?: 'ProfileIcon';
url?: Maybe<Scalars['String']>;
initials?: Maybe<Scalars['String']>;
bgColor?: Maybe<Scalars['String']>;
};
export type ProjectMember = {
@ -71,6 +82,7 @@ export type Project = {
owner: ProjectMember;
taskGroups: Array<TaskGroup>;
members: Array<ProjectMember>;
labels: Array<ProjectLabel>;
};
export type TaskGroup = {
@ -222,6 +234,11 @@ export type AssignTaskInput = {
userID: Scalars['UUID'];
};
export type UnassignTaskInput = {
taskID: Scalars['UUID'];
userID: Scalars['UUID'];
};
export type UpdateTaskDescriptionInput = {
taskID: Scalars['UUID'];
description: Scalars['String'];
@ -237,12 +254,19 @@ export type RemoveTaskLabelInput = {
taskLabelID: Scalars['UUID'];
};
export type NewProjectLabel = {
projectID: Scalars['UUID'];
labelColorID: Scalars['UUID'];
name?: Maybe<Scalars['String']>;
};
export type Mutation = {
__typename?: 'Mutation';
createRefreshToken: RefreshToken;
createUserAccount: UserAccount;
createTeam: Team;
createProject: Project;
createProjectLabel: ProjectLabel;
createTaskGroup: TaskGroup;
updateTaskGroupLocation: TaskGroup;
deleteTaskGroup: DeleteTaskGroupPayload;
@ -254,6 +278,7 @@ export type Mutation = {
updateTaskName: Task;
deleteTask: DeleteTaskPayload;
assignTask: Task;
unassignTask: Task;
logoutUser: Scalars['Boolean'];
};
@ -278,6 +303,11 @@ export type MutationCreateProjectArgs = {
};
export type MutationCreateProjectLabelArgs = {
input: NewProjectLabel;
};
export type MutationCreateTaskGroupArgs = {
input: NewTaskGroup;
};
@ -333,6 +363,11 @@ export type MutationAssignTaskArgs = {
};
export type MutationUnassignTaskArgs = {
input?: Maybe<UnassignTaskInput>;
};
export type MutationLogoutUserArgs = {
input: LogoutUser;
};
@ -438,7 +473,7 @@ export type FindProjectQuery = (
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials'>
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)>, taskGroups: Array<(
{ __typename?: 'TaskGroup' }
@ -446,6 +481,14 @@ export type FindProjectQuery = (
& { tasks: Array<(
{ __typename?: 'Task' }
& Pick<Task, 'taskID' | 'name' | 'position' | 'description'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)> }
)> }
)> }
) }
@ -469,7 +512,7 @@ export type FindTaskQuery = (
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials'>
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
) }
)> }
) }
@ -500,11 +543,29 @@ export type MeQuery = (
& Pick<UserAccount, 'firstName' | 'lastName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials'>
& Pick<ProfileIcon, 'initials' | 'bgColor'>
) }
) }
);
export type UnassignTaskMutationVariables = {
taskID: Scalars['UUID'];
userID: Scalars['UUID'];
};
export type UnassignTaskMutation = (
{ __typename?: 'Mutation' }
& { unassignTask: (
{ __typename?: 'Task' }
& Pick<Task, 'taskID'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'userID' | 'firstName' | 'lastName'>
)> }
) }
);
export type UpdateTaskDescriptionMutationVariables = {
taskID: Scalars['UUID'];
description: Scalars['String'];
@ -759,6 +820,7 @@ export const FindProjectDocument = gql`
profileIcon {
url
initials
bgColor
}
}
taskGroups {
@ -770,6 +832,16 @@ export const FindProjectDocument = gql`
name
position
description
assigned {
userID
firstName
lastName
profileIcon {
url
initials
bgColor
}
}
}
}
}
@ -818,6 +890,7 @@ export const FindTaskDocument = gql`
profileIcon {
url
initials
bgColor
}
}
}
@ -893,6 +966,7 @@ export const MeDocument = gql`
lastName
profileIcon {
initials
bgColor
}
}
}
@ -922,6 +996,44 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = ApolloReactCommon.QueryResult<MeQuery, MeQueryVariables>;
export const UnassignTaskDocument = gql`
mutation unassignTask($taskID: UUID!, $userID: UUID!) {
unassignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
userID
firstName
lastName
}
taskID
}
}
`;
export type UnassignTaskMutationFn = ApolloReactCommon.MutationFunction<UnassignTaskMutation, UnassignTaskMutationVariables>;
/**
* __useUnassignTaskMutation__
*
* To run a mutation, you first call `useUnassignTaskMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUnassignTaskMutation` 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 [unassignTaskMutation, { data, loading, error }] = useUnassignTaskMutation({
* variables: {
* taskID: // value for 'taskID'
* userID: // value for 'userID'
* },
* });
*/
export function useUnassignTaskMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UnassignTaskMutation, UnassignTaskMutationVariables>) {
return ApolloReactHooks.useMutation<UnassignTaskMutation, UnassignTaskMutationVariables>(UnassignTaskDocument, baseOptions);
}
export type UnassignTaskMutationHookResult = ReturnType<typeof useUnassignTaskMutation>;
export type UnassignTaskMutationResult = ApolloReactCommon.MutationResult<UnassignTaskMutation>;
export type UnassignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<UnassignTaskMutation, UnassignTaskMutationVariables>;
export const UpdateTaskDescriptionDocument = gql`
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {

View File

@ -8,6 +8,7 @@ query findProject($projectId: String!) {
profileIcon {
url
initials
bgColor
}
}
taskGroups {
@ -19,6 +20,16 @@ query findProject($projectId: String!) {
name
position
description
assigned {
userID
firstName
lastName
profileIcon {
url
initials
bgColor
}
}
}
}
}

View File

@ -14,6 +14,7 @@ query findTask($taskID: UUID!) {
profileIcon {
url
initials
bgColor
}
}
}

View File

@ -4,6 +4,7 @@ query me {
lastName
profileIcon {
initials
bgColor
}
}
}

View File

@ -0,0 +1,10 @@
mutation unassignTask($taskID: UUID!, $userID: UUID!) {
unassignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
userID
firstName
lastName
}
taskID
}
}