From 68fa7aef94599d0d700f40c43a0194aaa0c0bb21 Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Fri, 17 Jul 2020 19:40:05 -0500 Subject: [PATCH] feature: add user project count to Admin component --- frontend/src/Admin/index.tsx | 49 +- .../src/Projects/Project/Details/index.tsx | 5 +- frontend/src/Projects/Project/index.tsx | 2 + frontend/src/Teams/Members/index.tsx | 4 +- frontend/src/citadel.d.ts | 25 +- .../shared/components/Admin/Admin.stories.tsx | 22 +- .../src/shared/components/Admin/index.tsx | 85 ++-- .../src/shared/components/Input/index.tsx | 44 +- frontend/src/shared/generated/graphql.tsx | 178 ++++++- frontend/src/shared/graphql/findProject.ts | 20 + frontend/src/shared/graphql/team/getTeam.ts | 44 +- frontend/src/shared/graphql/users.graphqls | 20 + internal/db/project.sql.go | 60 +++ internal/db/querier.go | 4 + internal/db/query/project.sql | 6 + internal/db/query/team.sql | 6 + internal/db/team.sql.go | 60 +++ internal/graph/generated.go | 463 +++++++++++++++++- internal/graph/helpers.go | 48 ++ internal/graph/models_gen.go | 13 +- internal/graph/schema.graphqls | 15 +- internal/graph/schema.resolvers.go | 92 ++-- internal/graph/schema/_models.gql | 15 +- 23 files changed, 1140 insertions(+), 140 deletions(-) create mode 100644 internal/graph/helpers.go diff --git a/frontend/src/Admin/index.tsx b/frontend/src/Admin/index.tsx index 9c1f0d8..7617a48 100644 --- a/frontend/src/Admin/index.tsx +++ b/frontend/src/Admin/index.tsx @@ -12,7 +12,7 @@ import { import Input from 'shared/components/Input'; import styled from 'styled-components'; import Button from 'shared/components/Button'; -import { useForm } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; import { usePopup, Popup } from 'shared/components/PopupMenu'; import produce from 'immer'; import updateApolloCache from 'shared/utils/cache'; @@ -45,14 +45,19 @@ const DeleteUserPopup: React.FC = ({ onDeleteUser }) => { ); }; +type RoleCodeOption = { + label: string; + value: string; +}; type CreateUserData = { email: string; username: string; fullName: string; initials: string; password: string; - roleCode: string; + roleCode: RoleCodeOption; }; + const CreateUserForm = styled.form` display: flex; flex-direction: column; @@ -78,11 +83,11 @@ type AddUserPopupProps = { }; const AddUserPopup: React.FC = ({ onAddUser }) => { - const { register, handleSubmit, errors, setValue } = useForm(); - const [role, setRole] = useState(null); - register({ name: 'roleCode' }, { required: true }); + const { register, handleSubmit, errors, setValue, control } = useForm(); + console.log(errors); const createUser = (data: CreateUserData) => { + console.log(data); onAddUser(data); }; return ( @@ -106,19 +111,23 @@ const AddUserPopup: React.FC = ({ onAddUser }) => { variant="alternate" ref={register({ required: 'Email is required' })} /> - + )} /> - {errors.email && {errors.email.message}} + {errors.roleCode && errors.roleCode.value && {errors.roleCode.value.message}} = ({ onAddUser }) => { id="password" name="password" variant="alternate" + type="password" ref={register({ required: 'Password is required' })} /> {errors.password && {errors.password.message}} @@ -196,7 +206,7 @@ const AdminRoute = () => { <> {}} name={null} /> {}} onUpdateUserPassword={(user, password) => { @@ -223,7 +233,8 @@ const AdminRoute = () => { hidePopup()}> { - createUser({ variables: { ...user } }); + const { roleCode, ...userData } = user; + createUser({ variables: { ...userData, roleCode: roleCode.value } }); hidePopup(); }} /> diff --git a/frontend/src/Projects/Project/Details/index.tsx b/frontend/src/Projects/Project/Details/index.tsx index d6dbfa5..492d0c8 100644 --- a/frontend/src/Projects/Project/Details/index.tsx +++ b/frontend/src/Projects/Project/Details/index.tsx @@ -87,11 +87,12 @@ const CreateChecklistPopup: React.FC = ({ onCreateChe const createUser = (data: CreateChecklistData) => { onCreateChecklist(data); }; - console.log(errors); return ( = ({ showPopup( $target, { hidePopup(); diff --git a/frontend/src/Projects/Project/index.tsx b/frontend/src/Projects/Project/index.tsx index 38f72b7..3d23d20 100644 --- a/frontend/src/Projects/Project/index.tsx +++ b/frontend/src/Projects/Project/index.tsx @@ -178,6 +178,8 @@ const Project = () => { FindProjectDocument, cache => produce(cache, draftCache => { + console.log(cache); + console.log(response); draftCache.findProject.members = cache.findProject.members.filter( m => m.id !== response.data.deleteProjectMember.member.id, ); diff --git a/frontend/src/Teams/Members/index.tsx b/frontend/src/Teams/Members/index.tsx index 88206ca..e95cc6a 100644 --- a/frontend/src/Teams/Members/index.tsx +++ b/frontend/src/Teams/Members/index.tsx @@ -159,8 +159,8 @@ export const RemoveMemberButton = styled(Button)` `; type TeamRoleManagerPopupProps = { currentUserID: string; - subject: TaskUser; - members: Array; + subject: User; + members: Array; warning?: string | null; canChangeRole: boolean; onChangeRole: (roleCode: RoleCode) => void; diff --git a/frontend/src/citadel.d.ts b/frontend/src/citadel.d.ts index 931f090..8df1928 100644 --- a/frontend/src/citadel.d.ts +++ b/frontend/src/citadel.d.ts @@ -22,13 +22,19 @@ type Role = { name: string; }; -type User = { +type UserProject = { id: string; - fullName: string; - username: string; - email: string; - role: Role; - profileIcon: ProfileIcon; + name: string; +}; + +type UserTeam = { + id: string; + name: string; +}; + +type RelatedList = { + teams: Array; + projects: Array; }; type OwnedList = { @@ -42,7 +48,12 @@ type TaskUser = { profileIcon: ProfileIcon; username?: string; role?: Role; - owned?: OwnedList | null; +}; + +type User = TaskUser & { + email?: string; + member: RelatedList; + owned: RelatedList; }; type RefreshTokenResponse = { diff --git a/frontend/src/shared/components/Admin/Admin.stories.tsx b/frontend/src/shared/components/Admin/Admin.stories.tsx index 66086ab..c35750b 100644 --- a/frontend/src/shared/components/Admin/Admin.stories.tsx +++ b/frontend/src/shared/components/Admin/Admin.stories.tsx @@ -1,18 +1,18 @@ -import React, {useRef} from 'react'; +import React, { useRef } from 'react'; import Admin from '.'; -import {theme} from 'App/ThemeStyles'; +import { theme } from 'App/ThemeStyles'; import NormalizeStyles from 'App/NormalizeStyles'; import BaseStyles from 'App/BaseStyles'; -import {ThemeProvider} from 'styled-components'; -import {action} from '@storybook/addon-actions'; +import { ThemeProvider } from 'styled-components'; +import { action } from '@storybook/addon-actions'; export default { component: Admin, title: 'Admin', parameters: { backgrounds: [ - {name: 'gray', value: '#f8f8f8', default: true}, - {name: 'white', value: '#ffffff'}, + { name: 'gray', value: '#f8f8f8', default: true }, + { name: 'white', value: '#ffffff' }, ], }, }; @@ -33,13 +33,21 @@ export const Default = () => { id: '1', username: 'jordanthedev', email: 'jordan@jordanthedev.com', - role: {code: 'admin', name: 'Admin'}, + role: { code: 'admin', name: 'Admin' }, fullName: 'Jordan Knott', profileIcon: { bgColor: '#fff', initials: 'JK', url: null, }, + owned: { + teams: [{ id: '1', name: 'Team' }], + projects: [{ id: '2', name: 'Project' }], + }, + member: { + teams: [], + projects: [], + }, }, ]} onAddUser={action('add user')} diff --git a/frontend/src/shared/components/Admin/index.tsx b/frontend/src/shared/components/Admin/index.tsx index a82c035..b56b2f0 100644 --- a/frontend/src/shared/components/Admin/index.tsx +++ b/frontend/src/shared/components/Admin/index.tsx @@ -509,7 +509,7 @@ const ListTable: React.FC = ({ users, onDeleteUser }) => { rowSelection="multiple" defaultColDef={data.defaultColDef} columnDefs={data.columnDefs} - rowData={users.map(u => ({ ...u, roleName: u.role.name }))} + rowData={users.map(u => ({ ...u, roleName: 'member' }))} frameworkComponents={data.frameworkComponents} onFirstDataRendered={params => { params.api.sizeColumnsToFit(); @@ -717,46 +717,49 @@ const Admin: React.FC = ({ - {users.map(member => ( - - {}} member={member} /> - - {member.fullName} - {`@${member.username}`} - - - On 6 projects - { - showPopup( - $target, - { - onUpdateUserPassword(user, password); - }} - canChangeRole={member.role && member.role.code !== 'owner'} - onChangeRole={roleCode => { - updateUserRole({ variables: { userID: member.id, roleCode } }); - }} - onRemoveFromTeam={ - member.role && member.role.code === 'owner' - ? undefined - : () => { - hidePopup(); - } - } - />, - ); - }} - > - Manage - - - - ))} + {users.map(member => { + const projectTotal = member.owned.projects.length + member.member.projects.length; + return ( + + {}} member={member} /> + + {member.fullName} + {`@${member.username}`} + + + {`On ${projectTotal} projects`} + { + showPopup( + $target, + { + onUpdateUserPassword(user, password); + }} + canChangeRole={(member.role && member.role.code !== 'owner') ?? false} + onChangeRole={roleCode => { + updateUserRole({ variables: { userID: member.id, roleCode } }); + }} + onRemoveFromTeam={ + member.role && member.role.code === 'owner' + ? undefined + : () => { + hidePopup(); + } + } + />, + ); + }} + > + Manage + + + + ); + })} diff --git a/frontend/src/shared/components/Input/index.tsx b/frontend/src/shared/components/Input/index.tsx index c640ef3..2e82d3d 100644 --- a/frontend/src/shared/components/Input/index.tsx +++ b/frontend/src/shared/components/Input/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import styled, { css } from 'styled-components/macro'; const InputWrapper = styled.div<{ width: string }>` @@ -85,6 +85,8 @@ type InputProps = { icon?: JSX.Element; type?: string; autocomplete?: boolean; + autoFocus?: boolean; + autoSelect?: boolean; id?: string; name?: string; className?: string; @@ -92,12 +94,32 @@ type InputProps = { onClick?: (e: React.MouseEvent) => void; }; +function useCombinedRefs(...refs: any) { + const targetRef = React.useRef(); + + React.useEffect(() => { + refs.forEach((ref: any) => { + if (!ref) return; + + if (typeof ref === 'function') { + ref(targetRef.current); + } else { + ref.current = targetRef.current; + } + }); + }, [refs]); + + return targetRef; +} + const Input = React.forwardRef( ( { width = 'auto', variant = 'normal', type = 'text', + autoFocus = false, + autoSelect = false, autocomplete, label, placeholder, @@ -111,9 +133,25 @@ const Input = React.forwardRef( }: InputProps, $ref: any, ) => { - const [hasValue, setHasValue] = useState(false); + const [hasValue, setHasValue] = useState(defaultValue !== ''); const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561'; const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)'; + + // Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect + // The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for + // TODO(jordanknott): This is super ugly, find a better approach? + const $innerRef = React.useRef(null); + const combinedRef: any = useCombinedRefs($ref, $innerRef); + useEffect(() => { + if (combinedRef && combinedRef.current) { + if (autoFocus) { + combinedRef.current.focus(); + } + if (autoSelect) { + combinedRef.current.select(); + } + } + }, []); return ( ; + owned: OwnedList; + member: MemberList; }; export type RefreshToken = { @@ -84,6 +85,18 @@ export type Role = { name: Scalars['String']; }; +export type OwnedList = { + __typename?: 'OwnedList'; + teams: Array; + projects: Array; +}; + +export type MemberList = { + __typename?: 'MemberList'; + teams: Array; + projects: Array; +}; + export type UserAccount = { __typename?: 'UserAccount'; id: Scalars['ID']; @@ -94,6 +107,8 @@ export type UserAccount = { role: Role; username: Scalars['String']; profileIcon: ProfileIcon; + owned: OwnedList; + member: MemberList; }; export type Team = { @@ -1096,6 +1111,24 @@ export type FindProjectQuery = ( ), profileIcon: ( { __typename?: 'ProfileIcon' } & Pick + ), owned: ( + { __typename?: 'OwnedList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } + ), member: ( + { __typename?: 'MemberList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } ) } )> } ); @@ -1611,12 +1644,27 @@ export type GetTeamQuery = ( & { role: ( { __typename?: 'Role' } & Pick - ), owned?: Maybe<( - { __typename?: 'OwnersList' } - & Pick - )>, profileIcon: ( + ), profileIcon: ( { __typename?: 'ProfileIcon' } & Pick + ), owned: ( + { __typename?: 'OwnedList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } + ), member: ( + { __typename?: 'MemberList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } ) } )> } ), projects: Array<( @@ -1635,6 +1683,24 @@ export type GetTeamQuery = ( ), profileIcon: ( { __typename?: 'ProfileIcon' } & Pick + ), owned: ( + { __typename?: 'OwnedList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } + ), member: ( + { __typename?: 'MemberList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } ) } )> } ); @@ -1876,6 +1942,24 @@ export type UsersQuery = ( ), profileIcon: ( { __typename?: 'ProfileIcon' } & Pick + ), owned: ( + { __typename?: 'OwnedList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } + ), member: ( + { __typename?: 'MemberList' } + & { teams: Array<( + { __typename?: 'Team' } + & Pick + )>, projects: Array<( + { __typename?: 'Project' } + & Pick + )> } ) } )> } ); @@ -2278,6 +2362,26 @@ export const FindProjectDocument = gql` initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } } ${TaskFieldsFragmentDoc}`; @@ -3288,15 +3392,31 @@ export const GetTeamDocument = gql` code name } - owned { - projects - teams - } profileIcon { url initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } } projects(input: {teamID: $teamID}) { @@ -3321,6 +3441,26 @@ export const GetTeamDocument = gql` initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } } `; @@ -3834,6 +3974,26 @@ export const UsersDocument = gql` initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } } `; diff --git a/frontend/src/shared/graphql/findProject.ts b/frontend/src/shared/graphql/findProject.ts index 1012c43..ffa0e36 100644 --- a/frontend/src/shared/graphql/findProject.ts +++ b/frontend/src/shared/graphql/findProject.ts @@ -59,6 +59,26 @@ query findProject($projectId: String!) { initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } ${TASK_FRAGMENT} } diff --git a/frontend/src/shared/graphql/team/getTeam.ts b/frontend/src/shared/graphql/team/getTeam.ts index 22baf64..ec54839 100644 --- a/frontend/src/shared/graphql/team/getTeam.ts +++ b/frontend/src/shared/graphql/team/getTeam.ts @@ -14,15 +14,31 @@ export const GET_TEAM_QUERY = gql` code name } - owned { - projects - teams - } profileIcon { url initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } } projects(input: { teamID: $teamID }) { @@ -47,6 +63,26 @@ export const GET_TEAM_QUERY = gql` initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } } `; diff --git a/frontend/src/shared/graphql/users.graphqls b/frontend/src/shared/graphql/users.graphqls index f8c7074..61bf553 100644 --- a/frontend/src/shared/graphql/users.graphqls +++ b/frontend/src/shared/graphql/users.graphqls @@ -13,6 +13,26 @@ query users { initials bgColor } + owned { + teams { + id + name + } + projects { + id + name + } + } + member { + teams { + id + name + } + projects { + id + name + } + } } } diff --git a/internal/db/project.sql.go b/internal/db/project.sql.go index ff1420e..3c36291 100644 --- a/internal/db/project.sql.go +++ b/internal/db/project.sql.go @@ -158,6 +158,66 @@ func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ( return items, nil } +const getMemberProjectIDsForUserID = `-- name: GetMemberProjectIDsForUserID :many +SELECT project_id FROM project_member WHERE user_id = $1 +` + +func (q *Queries) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) { + rows, err := q.db.QueryContext(ctx, getMemberProjectIDsForUserID, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []uuid.UUID + for rows.Next() { + var project_id uuid.UUID + if err := rows.Scan(&project_id); err != nil { + return nil, err + } + items = append(items, project_id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getOwnedProjectsForUserID = `-- name: GetOwnedProjectsForUserID :many +SELECT project_id, team_id, created_at, name, owner FROM project WHERE owner = $1 +` + +func (q *Queries) GetOwnedProjectsForUserID(ctx context.Context, owner uuid.UUID) ([]Project, error) { + rows, err := q.db.QueryContext(ctx, getOwnedProjectsForUserID, owner) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Project + for rows.Next() { + var i Project + if err := rows.Scan( + &i.ProjectID, + &i.TeamID, + &i.CreatedAt, + &i.Name, + &i.Owner, + ); 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 +} + const getOwnedTeamProjectsForUserID = `-- name: GetOwnedTeamProjectsForUserID :many SELECT project_id FROM project WHERE owner = $1 AND team_id = $2 ` diff --git a/internal/db/querier.go b/internal/db/querier.go index eb20b51..897e0c6 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -52,7 +52,11 @@ type Querier interface { GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) GetLabelColors(ctx context.Context) ([]LabelColor, error) + GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) + GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) + GetOwnedProjectsForUserID(ctx context.Context, owner uuid.UUID) ([]Project, error) GetOwnedTeamProjectsForUserID(ctx context.Context, arg GetOwnedTeamProjectsForUserIDParams) ([]uuid.UUID, error) + GetOwnedTeamsForUserID(ctx context.Context, owner uuid.UUID) ([]Team, error) GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error) GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error) GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error) diff --git a/internal/db/query/project.sql b/internal/db/query/project.sql index 32b1a74..8470165 100644 --- a/internal/db/query/project.sql +++ b/internal/db/query/project.sql @@ -39,3 +39,9 @@ UPDATE project_member SET role_code = $3 WHERE project_id = $1 AND user_id = $2 -- name: GetOwnedTeamProjectsForUserID :many SELECT project_id FROM project WHERE owner = $1 AND team_id = $2; + +-- name: GetOwnedProjectsForUserID :many +SELECT * FROM project WHERE owner = $1; + +-- name: GetMemberProjectIDsForUserID :many +SELECT project_id FROM project_member WHERE user_id = $1; diff --git a/internal/db/query/team.sql b/internal/db/query/team.sql index 7ff17fe..9c35860 100644 --- a/internal/db/query/team.sql +++ b/internal/db/query/team.sql @@ -15,3 +15,9 @@ SELECT * FROM team WHERE organization_id = $1; -- name: SetTeamOwner :one UPDATE team SET owner = $2 WHERE team_id = $1 RETURNING *; + +-- name: GetOwnedTeamsForUserID :many +SELECT * FROM team WHERE owner = $1; + +-- name: GetMemberTeamIDsForUserID :many +SELECT team_id FROM team_member WHERE user_id = $1; diff --git a/internal/db/team.sql.go b/internal/db/team.sql.go index 8ae9455..8b66db4 100644 --- a/internal/db/team.sql.go +++ b/internal/db/team.sql.go @@ -81,6 +81,66 @@ func (q *Queries) GetAllTeams(ctx context.Context) ([]Team, error) { return items, nil } +const getMemberTeamIDsForUserID = `-- name: GetMemberTeamIDsForUserID :many +SELECT team_id FROM team_member WHERE user_id = $1 +` + +func (q *Queries) GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) { + rows, err := q.db.QueryContext(ctx, getMemberTeamIDsForUserID, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []uuid.UUID + for rows.Next() { + var team_id uuid.UUID + if err := rows.Scan(&team_id); err != nil { + return nil, err + } + items = append(items, team_id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getOwnedTeamsForUserID = `-- name: GetOwnedTeamsForUserID :many +SELECT team_id, created_at, name, organization_id, owner FROM team WHERE owner = $1 +` + +func (q *Queries) GetOwnedTeamsForUserID(ctx context.Context, owner uuid.UUID) ([]Team, error) { + rows, err := q.db.QueryContext(ctx, getOwnedTeamsForUserID, owner) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Team + for rows.Next() { + var i Team + if err := rows.Scan( + &i.TeamID, + &i.CreatedAt, + &i.Name, + &i.OrganizationID, + &i.Owner, + ); 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 +} + const getTeamByID = `-- name: GetTeamByID :one SELECT team_id, created_at, name, organization_id, owner FROM team WHERE team_id = $1 ` diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 0145b71..d584c26 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -130,12 +130,18 @@ type ComplexityRoot struct { Member struct { FullName func(childComplexity int) int ID func(childComplexity int) int + Member func(childComplexity int) int Owned func(childComplexity int) int ProfileIcon func(childComplexity int) int Role func(childComplexity int) int Username func(childComplexity int) int } + MemberList struct { + Projects func(childComplexity int) int + Teams func(childComplexity int) int + } + Mutation struct { AddTaskLabel func(childComplexity int, input *AddTaskLabelInput) int AssignTask func(childComplexity int, input *AssignTaskInput) int @@ -194,6 +200,11 @@ type ComplexityRoot struct { Name func(childComplexity int) int } + OwnedList struct { + Projects func(childComplexity int) int + Teams func(childComplexity int) int + } + OwnersList struct { Projects func(childComplexity int) int Teams func(childComplexity int) int @@ -363,6 +374,8 @@ type ComplexityRoot struct { FullName func(childComplexity int) int ID func(childComplexity int) int Initials func(childComplexity int) int + Member func(childComplexity int) int + Owned func(childComplexity int) int ProfileIcon func(childComplexity int) int Role func(childComplexity int) int Username func(childComplexity int) int @@ -501,6 +514,8 @@ type UserAccountResolver interface { Role(ctx context.Context, obj *db.UserAccount) (*db.Role, error) ProfileIcon(ctx context.Context, obj *db.UserAccount) (*ProfileIcon, error) + Owned(ctx context.Context, obj *db.UserAccount) (*OwnedList, error) + Member(ctx context.Context, obj *db.UserAccount) (*MemberList, error) } type executableSchema struct { @@ -749,6 +764,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Member.ID(childComplexity), true + case "Member.member": + if e.complexity.Member.Member == nil { + break + } + + return e.complexity.Member.Member(childComplexity), true + case "Member.owned": if e.complexity.Member.Owned == nil { break @@ -777,6 +799,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Member.Username(childComplexity), true + case "MemberList.projects": + if e.complexity.MemberList.Projects == nil { + break + } + + return e.complexity.MemberList.Projects(childComplexity), true + + case "MemberList.teams": + if e.complexity.MemberList.Teams == nil { + break + } + + return e.complexity.MemberList.Teams(childComplexity), true + case "Mutation.addTaskLabel": if e.complexity.Mutation.AddTaskLabel == nil { break @@ -1386,6 +1422,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Organization.Name(childComplexity), true + case "OwnedList.projects": + if e.complexity.OwnedList.Projects == nil { + break + } + + return e.complexity.OwnedList.Projects(childComplexity), true + + case "OwnedList.teams": + if e.complexity.OwnedList.Teams == nil { + break + } + + return e.complexity.OwnedList.Teams(childComplexity), true + case "OwnersList.projects": if e.complexity.OwnersList.Projects == nil { break @@ -2083,6 +2133,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.UserAccount.Initials(childComplexity), true + case "UserAccount.member": + if e.complexity.UserAccount.Member == nil { + break + } + + return e.complexity.UserAccount.Member(childComplexity), true + + case "UserAccount.owned": + if e.complexity.UserAccount.Owned == nil { + break + } + + return e.complexity.UserAccount.Owned(childComplexity), true + case "UserAccount.profileIcon": if e.complexity.UserAccount.ProfileIcon == nil { break @@ -2216,7 +2280,8 @@ type Member { fullName: String! username: String! profileIcon: ProfileIcon! - owned: OwnersList + owned: OwnedList! + member: MemberList! } type RefreshToken { @@ -2231,6 +2296,16 @@ type Role { name: String! } +type OwnedList { + teams: [Team!]! + projects: [Project!]! +} + +type MemberList { + teams: [Team!]! + projects: [Project!]! +} + type UserAccount { id: ID! email: String! @@ -2240,6 +2315,8 @@ type UserAccount { role: Role! username: String! profileIcon: ProfileIcon! + owned: OwnedList! + member: MemberList! } type Team { @@ -4851,11 +4928,116 @@ func (ec *executionContext) _Member_owned(ctx context.Context, field graphql.Col return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*OwnersList) + res := resTmp.(*OwnedList) fc.Result = res - return ec.marshalOOwnersList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐOwnersList(ctx, field.Selections, res) + return ec.marshalNOwnedList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐOwnedList(ctx, field.Selections, res) +} + +func (ec *executionContext) _Member_member(ctx context.Context, field graphql.CollectedField, obj *Member) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Member", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Member, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*MemberList) + fc.Result = res + return ec.marshalNMemberList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐMemberList(ctx, field.Selections, res) +} + +func (ec *executionContext) _MemberList_teams(ctx context.Context, field graphql.CollectedField, obj *MemberList) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MemberList", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Teams, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]db.Team) + fc.Result = res + return ec.marshalNTeam2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐTeamᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) _MemberList_projects(ctx context.Context, field graphql.CollectedField, obj *MemberList) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MemberList", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Projects, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]db.Project) + fc.Result = res + return ec.marshalNProject2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐProjectᚄ(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -6969,6 +7151,74 @@ func (ec *executionContext) _Organization_name(ctx context.Context, field graphq return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _OwnedList_teams(ctx context.Context, field graphql.CollectedField, obj *OwnedList) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "OwnedList", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Teams, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]db.Team) + fc.Result = res + return ec.marshalNTeam2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐTeamᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) _OwnedList_projects(ctx context.Context, field graphql.CollectedField, obj *OwnedList) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "OwnedList", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Projects, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]db.Project) + fc.Result = res + return ec.marshalNProject2ᚕgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋdbᚐProjectᚄ(ctx, field.Selections, res) +} + func (ec *executionContext) _OwnersList_projects(ctx context.Context, field graphql.CollectedField, obj *OwnersList) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -10418,6 +10668,74 @@ func (ec *executionContext) _UserAccount_profileIcon(ctx context.Context, field return ec.marshalNProfileIcon2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐProfileIcon(ctx, field.Selections, res) } +func (ec *executionContext) _UserAccount_owned(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "UserAccount", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.UserAccount().Owned(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*OwnedList) + fc.Result = res + return ec.marshalNOwnedList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐOwnedList(ctx, field.Selections, res) +} + +func (ec *executionContext) _UserAccount_member(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "UserAccount", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.UserAccount().Member(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*MemberList) + fc.Result = res + return ec.marshalNMemberList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐMemberList(ctx, field.Selections, res) +} + func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -13265,6 +13583,46 @@ func (ec *executionContext) _Member(ctx context.Context, sel ast.SelectionSet, o } case "owned": out.Values[i] = ec._Member_owned(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "member": + out.Values[i] = ec._Member_member(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var memberListImplementors = []string{"MemberList"} + +func (ec *executionContext) _MemberList(ctx context.Context, sel ast.SelectionSet, obj *MemberList) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, memberListImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("MemberList") + case "teams": + out.Values[i] = ec._MemberList_teams(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "projects": + out.Values[i] = ec._MemberList_projects(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -13593,6 +13951,38 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection return out } +var ownedListImplementors = []string{"OwnedList"} + +func (ec *executionContext) _OwnedList(ctx context.Context, sel ast.SelectionSet, obj *OwnedList) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, ownedListImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("OwnedList") + case "teams": + out.Values[i] = ec._OwnedList_teams(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "projects": + out.Values[i] = ec._OwnedList_projects(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var ownersListImplementors = []string{"OwnersList"} func (ec *executionContext) _OwnersList(ctx context.Context, sel ast.SelectionSet, obj *OwnersList) graphql.Marshaler { @@ -15001,6 +15391,34 @@ func (ec *executionContext) _UserAccount(ctx context.Context, sel ast.SelectionS } return res }) + case "owned": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._UserAccount_owned(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) + case "member": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._UserAccount_member(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -15645,6 +16063,20 @@ func (ec *executionContext) marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋproje return ec._Member(ctx, sel, v) } +func (ec *executionContext) marshalNMemberList2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐMemberList(ctx context.Context, sel ast.SelectionSet, v MemberList) graphql.Marshaler { + return ec._MemberList(ctx, sel, &v) +} + +func (ec *executionContext) marshalNMemberList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐMemberList(ctx context.Context, sel ast.SelectionSet, v *MemberList) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._MemberList(ctx, sel, v) +} + func (ec *executionContext) unmarshalNNewProject2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐNewProject(ctx context.Context, v interface{}) (NewProject, error) { return ec.unmarshalInputNewProject(ctx, v) } @@ -15722,6 +16154,20 @@ func (ec *executionContext) marshalNOrganization2ᚕgithubᚗcomᚋjordanknott return ret } +func (ec *executionContext) marshalNOwnedList2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐOwnedList(ctx context.Context, sel ast.SelectionSet, v OwnedList) graphql.Marshaler { + return ec._OwnedList(ctx, sel, &v) +} + +func (ec *executionContext) marshalNOwnedList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐOwnedList(ctx context.Context, sel ast.SelectionSet, v *OwnedList) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._OwnedList(ctx, sel, v) +} + func (ec *executionContext) marshalNProfileIcon2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐProfileIcon(ctx context.Context, sel ast.SelectionSet, v ProfileIcon) graphql.Marshaler { return ec._ProfileIcon(ctx, sel, &v) } @@ -16829,17 +17275,6 @@ func (ec *executionContext) marshalOChecklistBadge2ᚖgithubᚗcomᚋjordanknott return ec._ChecklistBadge(ctx, sel, v) } -func (ec *executionContext) marshalOOwnersList2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐOwnersList(ctx context.Context, sel ast.SelectionSet, v OwnersList) graphql.Marshaler { - return ec._OwnersList(ctx, sel, &v) -} - -func (ec *executionContext) marshalOOwnersList2ᚖgithubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐOwnersList(ctx context.Context, sel ast.SelectionSet, v *OwnersList) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._OwnersList(ctx, sel, v) -} - func (ec *executionContext) unmarshalOProjectsFilter2githubᚗcomᚋjordanknottᚋprojectᚑcitadelᚋapiᚋinternalᚋgraphᚐProjectsFilter(ctx context.Context, v interface{}) (ProjectsFilter, error) { return ec.unmarshalInputProjectsFilter(ctx, v) } diff --git a/internal/graph/helpers.go b/internal/graph/helpers.go new file mode 100644 index 0000000..d9daba5 --- /dev/null +++ b/internal/graph/helpers.go @@ -0,0 +1,48 @@ +package graph + +import ( + "context" + "database/sql" + + "github.com/jordanknott/project-citadel/api/internal/db" +) + +func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*OwnedList, error) { + ownedTeams, err := r.GetOwnedTeamsForUserID(ctx, user.UserID) + if err != sql.ErrNoRows && err != nil { + return &OwnedList{}, err + } + ownedProjects, err := r.GetOwnedProjectsForUserID(ctx, user.UserID) + if err != sql.ErrNoRows && err != nil { + return &OwnedList{}, err + } + return &OwnedList{Teams: ownedTeams, Projects: ownedProjects}, nil +} +func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) { + projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID) + if err != sql.ErrNoRows && err != nil { + return &MemberList{}, err + } + var projects []db.Project + for _, projectID := range projectMemberIDs { + project, err := r.GetProjectByID(ctx, projectID) + if err != nil { + return &MemberList{}, err + } + projects = append(projects, project) + } + teamMemberIDs, err := r.GetMemberTeamIDsForUserID(ctx, user.UserID) + if err != sql.ErrNoRows && err != nil { + return &MemberList{}, err + } + var teams []db.Team + for _, teamID := range teamMemberIDs { + team, err := r.GetTeamByID(ctx, teamID) + if err != nil { + return &MemberList{}, err + } + teams = append(teams, team) + } + + return &MemberList{Teams: teams, Projects: projects}, nil +} diff --git a/internal/graph/models_gen.go b/internal/graph/models_gen.go index 6892837..3ada872 100644 --- a/internal/graph/models_gen.go +++ b/internal/graph/models_gen.go @@ -176,7 +176,13 @@ type Member struct { FullName string `json:"fullName"` Username string `json:"username"` ProfileIcon *ProfileIcon `json:"profileIcon"` - Owned *OwnersList `json:"owned"` + Owned *OwnedList `json:"owned"` + Member *MemberList `json:"member"` +} + +type MemberList struct { + Teams []db.Team `json:"teams"` + Projects []db.Project `json:"projects"` } type NewProject struct { @@ -232,6 +238,11 @@ type NewUserAccount struct { RoleCode string `json:"roleCode"` } +type OwnedList struct { + Teams []db.Team `json:"teams"` + Projects []db.Project `json:"projects"` +} + type OwnersList struct { Projects []uuid.UUID `json:"projects"` Teams []uuid.UUID `json:"teams"` diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index a3b9662..c0dbe69 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -46,7 +46,8 @@ type Member { fullName: String! username: String! profileIcon: ProfileIcon! - owned: OwnersList + owned: OwnedList! + member: MemberList! } type RefreshToken { @@ -61,6 +62,16 @@ type Role { name: String! } +type OwnedList { + teams: [Team!]! + projects: [Project!]! +} + +type MemberList { + teams: [Team!]! + projects: [Project!]! +} + type UserAccount { id: ID! email: String! @@ -70,6 +81,8 @@ type UserAccount { role: Role! username: String! profileIcon: ProfileIcon! + owned: OwnedList! + member: MemberList! } type Team { diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index e5c0f2d..fac32f0 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -113,16 +113,13 @@ func (r *mutationResolver) CreateProjectMember(ctx context.Context, input Create return &CreateProjectMemberPayload{Ok: true, Member: &Member{ ID: input.UserID, FullName: user.FullName, + Username: user.Username, ProfileIcon: profileIcon, Role: &db.Role{Code: role.Code, Name: role.Name}, }}, nil } func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) { - err := r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{UserID: input.UserID, ProjectID: input.ProjectID}) - if err != nil { - return &DeleteProjectMemberPayload{Ok: false}, err - } user, err := r.Repository.GetUserAccountByID(ctx, input.UserID) if err != nil { return &DeleteProjectMemberPayload{Ok: false}, err @@ -136,6 +133,10 @@ func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input Delete if err != nil { return &DeleteProjectMemberPayload{Ok: false}, err } + err = r.Repository.DeleteProjectMember(ctx, db.DeleteProjectMemberParams{UserID: input.UserID, ProjectID: input.ProjectID}) + if err != nil { + return &DeleteProjectMemberPayload{Ok: false}, err + } return &DeleteProjectMemberPayload{Ok: true, Member: &Member{ ID: input.UserID, FullName: user.FullName, @@ -1156,20 +1157,15 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err log.WithError(err).Error("get user account by ID") return members, err } - ownedProjects, err := r.Repository.GetOwnedTeamProjectsForUserID(ctx, db.GetOwnedTeamProjectsForUserIDParams{TeamID: obj.TeamID, Owner: user.UserID}) - log.WithFields(log.Fields{"projects": ownedProjects}).Info("retrieved owned project list") - if err == sql.ErrNoRows { - ownedProjects = []uuid.UUID{} - } else if err != nil { - log.WithError(err).Error("get owned team projects for user id") + ownedList, err := GetOwnedList(ctx, r.Repository, user) + if err != nil { return members, err } - ownedTeams := []uuid.UUID{} - var ownerList *OwnersList - if len(ownedTeams) != 0 || len(ownedProjects) != 0 { - log.Info("owned list is not empty") - ownerList = &OwnersList{Projects: ownedProjects, Teams: ownedTeams} + memberList, err := GetMemberList(ctx, r.Repository, user) + if err != nil { + return members, err } + var url *string if user.ProfileAvatarUrl.Valid { url = &user.ProfileAvatarUrl.String @@ -1177,7 +1173,7 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor} members = append(members, Member{ ID: obj.Owner, FullName: user.FullName, ProfileIcon: profileIcon, Username: user.Username, - Owned: ownerList, Role: &db.Role{Code: "owner", Name: "Owner"}, + Owned: ownedList, Member: memberList, Role: &db.Role{Code: "owner", Name: "Owner"}, }) teamMembers, err := r.Repository.GetTeamMembersForTeamID(ctx, obj.TeamID) if err != nil { @@ -1200,24 +1196,19 @@ func (r *teamResolver) Members(ctx context.Context, obj *db.Team) ([]Member, err log.WithError(err).Error("get role for projet member by user ID") return members, err } - ownedProjects, err := r.Repository.GetOwnedTeamProjectsForUserID(ctx, db.GetOwnedTeamProjectsForUserIDParams{TeamID: obj.TeamID, Owner: user.UserID}) - log.WithFields(log.Fields{"projects": ownedProjects}).Info("retrieved owned project list") - if err == sql.ErrNoRows { - ownedProjects = []uuid.UUID{} - } else if err != nil { - log.WithError(err).Error("get owned team projects for user id") + + ownedList, err := GetOwnedList(ctx, r.Repository, user) + if err != nil { return members, err } - ownedTeams := []uuid.UUID{} - var ownerList *OwnersList - if len(ownedTeams) != 0 || len(ownedProjects) != 0 { - log.Info("owned list is not empty") - ownerList = &OwnersList{Projects: ownedProjects, Teams: ownedTeams} + memberList, err := GetMemberList(ctx, r.Repository, user) + if err != nil { + return members, err } profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor} members = append(members, Member{ID: user.UserID, FullName: user.FullName, ProfileIcon: profileIcon, - Username: user.Username, Owned: ownerList, Role: &db.Role{Code: role.Code, Name: role.Name}, + Username: user.Username, Owned: ownedList, Member: memberList, Role: &db.Role{Code: role.Code, Name: role.Name}, }) } return members, nil @@ -1246,6 +1237,47 @@ func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *db.UserAccou return profileIcon, nil } +func (r *userAccountResolver) Owned(ctx context.Context, obj *db.UserAccount) (*OwnedList, error) { + ownedTeams, err := r.Repository.GetOwnedTeamsForUserID(ctx, obj.UserID) + if err != sql.ErrNoRows && err != nil { + return &OwnedList{}, err + } + ownedProjects, err := r.Repository.GetOwnedProjectsForUserID(ctx, obj.UserID) + if err != sql.ErrNoRows && err != nil { + return &OwnedList{}, err + } + return &OwnedList{Teams: ownedTeams, Projects: ownedProjects}, nil +} + +func (r *userAccountResolver) Member(ctx context.Context, obj *db.UserAccount) (*MemberList, error) { + projectMemberIDs, err := r.Repository.GetMemberProjectIDsForUserID(ctx, obj.UserID) + if err != sql.ErrNoRows && err != nil { + return &MemberList{}, err + } + var projects []db.Project + for _, projectID := range projectMemberIDs { + project, err := r.Repository.GetProjectByID(ctx, projectID) + if err != nil { + return &MemberList{}, err + } + projects = append(projects, project) + } + teamMemberIDs, err := r.Repository.GetMemberTeamIDsForUserID(ctx, obj.UserID) + if err != sql.ErrNoRows && err != nil { + return &MemberList{}, err + } + var teams []db.Team + for _, teamID := range teamMemberIDs { + team, err := r.Repository.GetTeamByID(ctx, teamID) + if err != nil { + return &MemberList{}, err + } + teams = append(teams, team) + } + + return &MemberList{Teams: teams, Projects: projects}, err +} + // LabelColor returns LabelColorResolver implementation. func (r *Resolver) LabelColor() LabelColorResolver { return &labelColorResolver{r} } @@ -1274,7 +1306,9 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} } func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} } // TaskChecklistItem returns TaskChecklistItemResolver implementation. -func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver { return &taskChecklistItemResolver{r} } +func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver { + return &taskChecklistItemResolver{r} +} // TaskGroup returns TaskGroupResolver implementation. func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} } diff --git a/internal/graph/schema/_models.gql b/internal/graph/schema/_models.gql index 683f47e..da069d8 100644 --- a/internal/graph/schema/_models.gql +++ b/internal/graph/schema/_models.gql @@ -46,7 +46,8 @@ type Member { fullName: String! username: String! profileIcon: ProfileIcon! - owned: OwnersList + owned: OwnedList! + member: MemberList! } type RefreshToken { @@ -61,6 +62,16 @@ type Role { name: String! } +type OwnedList { + teams: [Team!]! + projects: [Project!]! +} + +type MemberList { + teams: [Team!]! + projects: [Project!]! +} + type UserAccount { id: ID! email: String! @@ -70,6 +81,8 @@ type UserAccount { role: Role! username: String! profileIcon: ProfileIcon! + owned: OwnedList! + member: MemberList! } type Team {