From 737d2b640fc53fad628c3a725392eb6f26901c89 Mon Sep 17 00:00:00 2001 From: Jordan Knott Date: Tue, 29 Sep 2020 16:01:52 -0500 Subject: [PATCH] feat: redesign project sharing --- .../Project/LabelManagerEditor/index.tsx | 21 - frontend/src/Projects/Project/index.tsx | 190 +++++- .../src/shared/components/Button/index.tsx | 25 +- .../src/shared/components/Select/index.tsx | 2 +- .../shared/components/TaskAssignee/index.tsx | 42 +- frontend/src/shared/generated/graphql.tsx | 187 +++--- .../graphql/project/createProjectMember.ts | 25 - .../graphql/project/inviteProjectMember.ts | 25 + go.sum | 1 + internal/db/query/user_accounts.sql | 2 +- internal/db/user_accounts.sql.go | 10 +- internal/graph/generated.go | 572 ++++++++---------- internal/graph/models_gen.go | 32 +- internal/graph/schema.graphqls | 17 +- internal/graph/schema.resolvers.go | 70 ++- internal/graph/schema/project_member.gql | 12 +- internal/graph/schema/user.gql | 5 +- 17 files changed, 708 insertions(+), 530 deletions(-) delete mode 100644 frontend/src/shared/graphql/project/createProjectMember.ts create mode 100644 frontend/src/shared/graphql/project/inviteProjectMember.ts diff --git a/frontend/src/Projects/Project/LabelManagerEditor/index.tsx b/frontend/src/Projects/Project/LabelManagerEditor/index.tsx index 6291963..229ad98 100644 --- a/frontend/src/Projects/Project/LabelManagerEditor/index.tsx +++ b/frontend/src/Projects/Project/LabelManagerEditor/index.tsx @@ -3,32 +3,11 @@ import updateApolloCache from 'shared/utils/cache'; import { usePopup, Popup } from 'shared/components/PopupMenu'; import produce from 'immer'; import { - useUpdateProjectMemberRoleMutation, - useCreateProjectMemberMutation, - useDeleteProjectMemberMutation, - useSetTaskCompleteMutation, - useToggleTaskLabelMutation, - useUpdateProjectNameMutation, - useFindProjectQuery, - useUpdateTaskGroupNameMutation, - useUpdateTaskNameMutation, useUpdateProjectLabelMutation, - useCreateTaskMutation, useDeleteProjectLabelMutation, - useDeleteTaskMutation, - useUpdateTaskLocationMutation, - useUpdateTaskGroupLocationMutation, - useCreateTaskGroupMutation, - useDeleteTaskGroupMutation, - useUpdateTaskDescriptionMutation, - useAssignTaskMutation, - DeleteTaskDocument, FindProjectDocument, useCreateProjectLabelMutation, - useUnassignTaskMutation, - useUpdateTaskDueDateMutation, FindProjectQuery, - useUsersQuery, } from 'shared/generated/graphql'; import LabelManager from 'shared/components/PopupMenu/LabelManager'; import LabelEditor from 'shared/components/PopupMenu/LabelEditor'; diff --git a/frontend/src/Projects/Project/index.tsx b/frontend/src/Projects/Project/index.tsx index eee9c22..544353f 100644 --- a/frontend/src/Projects/Project/index.tsx +++ b/frontend/src/Projects/Project/index.tsx @@ -16,7 +16,7 @@ import { } from 'react-router-dom'; import { useUpdateProjectMemberRoleMutation, - useCreateProjectMemberMutation, + useInviteProjectMemberMutation, useDeleteProjectMemberMutation, useToggleTaskLabelMutation, useUpdateProjectNameMutation, @@ -38,10 +38,12 @@ import Input from 'shared/components/Input'; import Member from 'shared/components/Member'; import EmptyBoard from 'shared/components/EmptyBoard'; import NOOP from 'shared/utils/noop'; -import { Lock } from 'shared/icons'; +import { Lock, Cross } from 'shared/icons'; import Button from 'shared/components/Button'; import { useApolloClient } from '@apollo/react-hooks'; +import TaskAssignee from 'shared/components/TaskAssignee'; import gql from 'graphql-tag'; +import { colourStyles } from 'shared/components/Select'; import Board, { BoardLoading } from './Board'; import Details from './Details'; import LabelManagerEditor from './LabelManagerEditor'; @@ -77,10 +79,14 @@ const MemberList = styled.div` margin: 8px 0; `; +type InviteUserData = { + email?: string; + suerID?: string; +}; type UserManagementPopupProps = { users: Array; projectMembers: Array; - onAddProjectMember: (userID: string) => void; + onInviteProjectMember: (data: InviteUserData) => void; }; const VisibiltyPrivateIcon = styled(Lock)` @@ -133,40 +139,182 @@ const fetchMembers = async (client: any, options: MemberFilterOptions, input: st query: gql` query { searchMembers(input: {SearchFilter:"${input}"}) { - id similarity - username - fullName confirmed joined + user { + id + fullName + email + profileIcon { + url + initials + bgColor + } + } } } `, }); let results: any = []; + const emails: Array = []; if (res.data && res.data.searchMembers) { - results = [...res.data.searchMembers.map((m: any) => ({ label: m.fullName, value: m.id }))]; + results = [ + ...res.data.searchMembers.map((m: any) => { + emails.push(m.user.email); + return { + label: m.user.fullName, + value: { id: m.id, type: 0, profileIcon: m.user.profileIcon }, + }; + }), + ]; } - if (RFC2822_EMAIL.test(input)) { - results = [...results, { label: input, value: input }]; + if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) { + results = [ + ...results, + { + label: input, + value: { + id: input, + type: 1, + profileIcon: { + bgColor: '#ccc', + initials: input.charAt(0), + }, + }, + }, + ]; } return results; }; -const UserManagementPopup: React.FC = ({ users, projectMembers, onAddProjectMember }) => { +type UserOptionProps = { + innerProps: any; + isDisabled: boolean; + isFocused: boolean; + label: string; + data: any; + getValue: any; +}; + +const OptionWrapper = styled.div<{ isFocused: boolean }>` + cursor: pointer; + padding: 4px 8px; + ${props => props.isFocused && `background: rgba(${props.theme.colors.primary});`} + display: flex; + align-items: center; +`; +const OptionContent = styled.div` + display: flex; + flex-direction: column; + margin-left: 12px; +`; + +const UserOption: React.FC = ({ isDisabled, isFocused, innerProps, label, data }) => { + return !isDisabled ? ( + + + {label} + + ) : null; +}; + +const OptionValueWrapper = styled.div` + background: rgba(${props => props.theme.colors.bg.primary}); + border-radius: 4px; + margin: 2px; + padding: 3px 6px 3px 4px; + display: flex; + align-items: center; +`; + +const OptionValueLabel = styled.span` + font-size: 12px; + color: rgba(${props => props.theme.colors.text.secondary}); +`; + +const OptionValueRemove = styled.button` + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + outline: none; + padding: 0; + margin: 0; + margin-left: 4px; +`; +const OptionValue = ({ data, removeProps }: any) => { + return ( + + {data.label} + + + + + ); +}; + +const InviteButton = styled(Button)` + margin-top: 12px; + height: 32px; + padding: 4px 12px; + width: 100%; + justify-content: center; +`; + +const InviteContainer = styled.div` + min-height: 300px; + display: flex; + flex-direction: column; +`; + +const UserManagementPopup: React.FC = ({ users, projectMembers, onInviteProjectMember }) => { const client = useApolloClient(); + const [invitedUsers, setInvitedUsers] = useState | null>(null); return ( - fetchMembers(client, {}, i, cb)} /> - - - - Private - - + + null} + onChange={(e: any) => setInvitedUsers(e ? e.value : null)} + isMulti + autoFocus + cacheOptions + styles={colourStyles} + defaultOption + components={{ + MultiValue: OptionValue, + Option: UserOption, + IndicatorSeparator: null, + DropdownIndicator: null, + }} + loadOptions={(i, cb) => fetchMembers(client, {}, i, cb)} + /> + + { + // FUCK, gotta rewrite invite member to be MULTIPLE. SHIT! + // onInviteProjectMember(); + }} + disabled={invitedUsers === null} + hoverVariant="none" + fontSize="16px" + > + Send Invite + ); }; @@ -250,14 +398,14 @@ const Project = () => { }, }); - const [createProjectMember] = useCreateProjectMemberMutation({ + const [inviteProjectMember] = useInviteProjectMemberMutation({ update: (client, response) => { updateApolloCache( client, FindProjectDocument, cache => produce(cache, draftCache => { - draftCache.findProject.members.push({ ...response.data.createProjectMember.member }); + draftCache.findProject.members.push({ ...response.data.inviteProjectMember.member }); }), { projectID }, ); @@ -324,8 +472,8 @@ const Project = () => { showPopup( $target, { - createProjectMember({ variables: { userID, projectID } }); + onInviteProjectMember={userID => { + // /inviteProjectMember({ variables: { userID, projectID } }); }} users={data.users} projectMembers={data.findProject.members} diff --git a/frontend/src/shared/components/Button/index.tsx b/frontend/src/shared/components/Button/index.tsx index b977123..41f9380 100644 --- a/frontend/src/shared/components/Button/index.tsx +++ b/frontend/src/shared/components/Button/index.tsx @@ -35,11 +35,15 @@ const Base = styled.button<{ color: string; disabled: boolean }>` `} `; -const Filled = styled(Base)` +const Filled = styled(Base)<{ hoverVariant: HoverVariant }>` background: rgba(${props => props.theme.colors[props.color]}); - &:hover { - box-shadow: 0 8px 25px -8px rgba(${props => props.theme.colors[props.color]}); - } + ${props => + props.hoverVariant === 'boxShadow' && + css` + &:hover { + box-shadow: 0 8px 25px -8px rgba(${props.theme.colors[props.color]}); + } + `} `; const Outline = styled(Base)<{ invert: boolean }>` border: 1px solid rgba(${props => props.theme.colors[props.color]}); @@ -123,9 +127,11 @@ const Relief = styled(Base)` } `; +type HoverVariant = 'boxShadow' | 'none'; type ButtonProps = { fontSize?: string; variant?: 'filled' | 'outline' | 'flat' | 'lineDown' | 'gradient' | 'relief'; + hoverVariant?: HoverVariant; color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark'; disabled?: boolean; type?: 'button' | 'submit'; @@ -142,6 +148,7 @@ const Button: React.FC = ({ invert = false, color = 'primary', variant = 'filled', + hoverVariant = 'boxShadow', type = 'button', justifyTextContent = 'center', icon, @@ -158,7 +165,15 @@ const Button: React.FC = ({ switch (variant) { case 'filled': return ( - + {icon && icon} {children} diff --git a/frontend/src/shared/components/Select/index.tsx b/frontend/src/shared/components/Select/index.tsx index fad89d8..994d49b 100644 --- a/frontend/src/shared/components/Select/index.tsx +++ b/frontend/src/shared/components/Select/index.tsx @@ -16,7 +16,7 @@ function getBackgroundColor(isDisabled: boolean, isSelected: boolean, isFocused: return null; } -const colourStyles = { +export const colourStyles = { control: (styles: any, data: any) => { return { ...styles, diff --git a/frontend/src/shared/components/TaskAssignee/index.tsx b/frontend/src/shared/components/TaskAssignee/index.tsx index c0be2de..3c48fe4 100644 --- a/frontend/src/shared/components/TaskAssignee/index.tsx +++ b/frontend/src/shared/components/TaskAssignee/index.tsx @@ -1,5 +1,5 @@ import React, { useRef } from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { DoubleChevronUp, Crown } from 'shared/icons'; export const AdminIcon = styled(DoubleChevronUp)` @@ -24,7 +24,12 @@ const TaskDetailAssignee = styled.div` position: relative; `; -export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>` +export const Wrapper = styled.div<{ + size: number | string; + bgColor: string | null; + backgroundURL: string | null; + hasClick: boolean; +}>` width: ${props => props.size}px; height: ${props => props.size}px; border-radius: 9999px; @@ -37,33 +42,52 @@ export const Wrapper = styled.div<{ size: number | string; bgColor: string | nul background-size: contain; font-size: 14px; font-weight: 400; - &:hover { - opacity: 0.8; - } + ${props => + props.hasClick && + css` + &:hover { + opacity: 0.8; + } + `} `; type TaskAssigneeProps = { size: number | string; showRoleIcons?: boolean; member: TaskUser; - onMemberProfile: ($targetRef: React.RefObject, memberID: string) => void; + onMemberProfile?: ($targetRef: React.RefObject, memberID: string) => void; className?: string; }; const TaskAssignee: React.FC = ({ showRoleIcons, member, onMemberProfile, size, className }) => { const $memberRef = useRef(null); + let profileIcon: ProfileIcon = { + url: null, + bgColor: null, + initials: null, + }; + if (member.profileIcon) { + profileIcon = member.profileIcon; + } return ( { e.stopPropagation(); - onMemberProfile($memberRef, member.id); + if (onMemberProfile) { + onMemberProfile($memberRef, member.id); + } }} key={member.id} > - - {(!member.profileIcon.url && member.profileIcon.initials) ?? ''} + + {(!profileIcon.url && profileIcon.initials) ?? ''} {showRoleIcons && member.role && member.role.code === 'admin' && } {showRoleIcons && member.role && member.role.code === 'owner' && } diff --git a/frontend/src/shared/generated/graphql.tsx b/frontend/src/shared/generated/graphql.tsx index 675bbc2..b470c4c 100644 --- a/frontend/src/shared/generated/graphql.tsx +++ b/frontend/src/shared/generated/graphql.tsx @@ -226,6 +226,7 @@ export type Query = { notifications: Array; organizations: Array; projects: Array; + searchMembers: Array; taskGroups: Array; teams: Array; users: Array; @@ -256,6 +257,11 @@ export type QueryProjectsArgs = { input?: Maybe; }; + +export type QuerySearchMembersArgs = { + input: MemberSearchFilter; +}; + export type Mutation = { __typename?: 'Mutation'; addTaskLabel: Task; @@ -263,7 +269,6 @@ export type Mutation = { clearProfileAvatar: UserAccount; createProject: Project; createProjectLabel: ProjectLabel; - createProjectMember: CreateProjectMemberPayload; createRefreshToken: RefreshToken; createTask: Task; createTaskChecklist: TaskChecklist; @@ -284,6 +289,7 @@ export type Mutation = { deleteTeamMember: DeleteTeamMemberPayload; deleteUserAccount: DeleteUserAccountPayload; duplicateTaskGroup: DuplicateTaskGroupPayload; + inviteProjectMember: InviteProjectMemberPayload; logoutUser: Scalars['Boolean']; removeTaskLabel: Task; setTaskChecklistItemComplete: TaskChecklistItem; @@ -333,11 +339,6 @@ export type MutationCreateProjectLabelArgs = { }; -export type MutationCreateProjectMemberArgs = { - input: CreateProjectMember; -}; - - export type MutationCreateRefreshTokenArgs = { input: NewRefreshToken; }; @@ -438,6 +439,11 @@ export type MutationDuplicateTaskGroupArgs = { }; +export type MutationInviteProjectMemberArgs = { + input: InviteProjectMember; +}; + + export type MutationLogoutUserArgs = { input: LogoutUser; }; @@ -688,13 +694,14 @@ export type UpdateProjectLabelColor = { labelColorID: Scalars['UUID']; }; -export type CreateProjectMember = { +export type InviteProjectMember = { projectID: Scalars['UUID']; - userID: Scalars['UUID']; + userID?: Maybe; + email?: Maybe; }; -export type CreateProjectMemberPayload = { - __typename?: 'CreateProjectMemberPayload'; +export type InviteProjectMemberPayload = { + __typename?: 'InviteProjectMemberPayload'; ok: Scalars['Boolean']; member: Member; }; @@ -989,6 +996,20 @@ export type UpdateTeamMemberRolePayload = { member: Member; }; +export type MemberSearchFilter = { + SearchFilter: Scalars['String']; + projectID?: Maybe; +}; + +export type MemberSearchResult = { + __typename?: 'MemberSearchResult'; + similarity: Scalars['Int']; + user: UserAccount; + confirmed: Scalars['Boolean']; + invited: Scalars['Boolean']; + joined: Scalars['Boolean']; +}; + export type UpdateUserInfoPayload = { __typename?: 'UpdateUserInfoPayload'; user: UserAccount; @@ -1390,31 +1411,6 @@ export type MeQuery = ( ) } ); -export type CreateProjectMemberMutationVariables = { - projectID: Scalars['UUID']; - userID: Scalars['UUID']; -}; - - -export type CreateProjectMemberMutation = ( - { __typename?: 'Mutation' } - & { createProjectMember: ( - { __typename?: 'CreateProjectMemberPayload' } - & Pick - & { member: ( - { __typename?: 'Member' } - & Pick - & { profileIcon: ( - { __typename?: 'ProfileIcon' } - & Pick - ), role: ( - { __typename?: 'Role' } - & Pick - ) } - ) } - ) } -); - export type DeleteProjectMutationVariables = { projectID: Scalars['UUID']; }; @@ -1450,6 +1446,32 @@ export type DeleteProjectMemberMutation = ( ) } ); +export type InviteProjectMemberMutationVariables = { + projectID: Scalars['UUID']; + userID?: Maybe; + email?: Maybe; +}; + + +export type InviteProjectMemberMutation = ( + { __typename?: 'Mutation' } + & { inviteProjectMember: ( + { __typename?: 'InviteProjectMemberPayload' } + & Pick + & { member: ( + { __typename?: 'Member' } + & Pick + & { profileIcon: ( + { __typename?: 'ProfileIcon' } + & Pick + ), role: ( + { __typename?: 'Role' } + & Pick + ) } + ) } + ) } +); + export type UpdateProjectMemberRoleMutationVariables = { projectID: Scalars['UUID']; userID: Scalars['UUID']; @@ -2884,53 +2906,6 @@ export function useMeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptio export type MeQueryHookResult = ReturnType; export type MeLazyQueryHookResult = ReturnType; export type MeQueryResult = ApolloReactCommon.QueryResult; -export const CreateProjectMemberDocument = gql` - mutation createProjectMember($projectID: UUID!, $userID: UUID!) { - createProjectMember(input: {projectID: $projectID, userID: $userID}) { - ok - member { - id - fullName - profileIcon { - url - initials - bgColor - } - username - role { - code - name - } - } - } -} - `; -export type CreateProjectMemberMutationFn = ApolloReactCommon.MutationFunction; - -/** - * __useCreateProjectMemberMutation__ - * - * To run a mutation, you first call `useCreateProjectMemberMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useCreateProjectMemberMutation` 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 [createProjectMemberMutation, { data, loading, error }] = useCreateProjectMemberMutation({ - * variables: { - * projectID: // value for 'projectID' - * userID: // value for 'userID' - * }, - * }); - */ -export function useCreateProjectMemberMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { - return ApolloReactHooks.useMutation(CreateProjectMemberDocument, baseOptions); - } -export type CreateProjectMemberMutationHookResult = ReturnType; -export type CreateProjectMemberMutationResult = ApolloReactCommon.MutationResult; -export type CreateProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions; export const DeleteProjectDocument = gql` mutation deleteProject($projectID: UUID!) { deleteProject(input: {projectID: $projectID}) { @@ -3003,6 +2978,54 @@ export function useDeleteProjectMemberMutation(baseOptions?: ApolloReactHooks.Mu export type DeleteProjectMemberMutationHookResult = ReturnType; export type DeleteProjectMemberMutationResult = ApolloReactCommon.MutationResult; export type DeleteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const InviteProjectMemberDocument = gql` + mutation inviteProjectMember($projectID: UUID!, $userID: UUID, $email: String) { + inviteProjectMember(input: {projectID: $projectID, userID: $userID, email: $email}) { + ok + member { + id + fullName + profileIcon { + url + initials + bgColor + } + username + role { + code + name + } + } + } +} + `; +export type InviteProjectMemberMutationFn = ApolloReactCommon.MutationFunction; + +/** + * __useInviteProjectMemberMutation__ + * + * To run a mutation, you first call `useInviteProjectMemberMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInviteProjectMemberMutation` 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 [inviteProjectMemberMutation, { data, loading, error }] = useInviteProjectMemberMutation({ + * variables: { + * projectID: // value for 'projectID' + * userID: // value for 'userID' + * email: // value for 'email' + * }, + * }); + */ +export function useInviteProjectMemberMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(InviteProjectMemberDocument, baseOptions); + } +export type InviteProjectMemberMutationHookResult = ReturnType; +export type InviteProjectMemberMutationResult = ApolloReactCommon.MutationResult; +export type InviteProjectMemberMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UpdateProjectMemberRoleDocument = gql` mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) { updateProjectMemberRole(input: {projectID: $projectID, userID: $userID, roleCode: $roleCode}) { diff --git a/frontend/src/shared/graphql/project/createProjectMember.ts b/frontend/src/shared/graphql/project/createProjectMember.ts deleted file mode 100644 index ddad02d..0000000 --- a/frontend/src/shared/graphql/project/createProjectMember.ts +++ /dev/null @@ -1,25 +0,0 @@ -import gql from 'graphql-tag'; - -export const CREATE_PROJECT_MEMBER_MUTATION = gql` - mutation createProjectMember($projectID: UUID!, $userID: UUID!) { - createProjectMember(input: { projectID: $projectID, userID: $userID }) { - ok - member { - id - fullName - profileIcon { - url - initials - bgColor - } - username - role { - code - name - } - } - } - } -`; - -export default CREATE_PROJECT_MEMBER_MUTATION; diff --git a/frontend/src/shared/graphql/project/inviteProjectMember.ts b/frontend/src/shared/graphql/project/inviteProjectMember.ts new file mode 100644 index 0000000..bc00225 --- /dev/null +++ b/frontend/src/shared/graphql/project/inviteProjectMember.ts @@ -0,0 +1,25 @@ +import gql from 'graphql-tag'; + +export const INVITE_PROJECT_MEMBER_MUTATION = gql` + mutation inviteProjectMember($projectID: UUID!, $userID: UUID, $email: String) { + inviteProjectMember(input: { projectID: $projectID, userID: $userID, email: $email }) { + ok + member { + id + fullName + profileIcon { + url + initials + bgColor + } + username + role { + code + name + } + } + } + } +`; + +export default INVITE_PROJECT_MEMBER_MUTATION; diff --git a/go.sum b/go.sum index 1929136..b4d1550 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/db/query/user_accounts.sql b/internal/db/query/user_accounts.sql index 09d1bff..f98fa29 100644 --- a/internal/db/query/user_accounts.sql +++ b/internal/db/query/user_accounts.sql @@ -16,7 +16,7 @@ UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1 RETURNING *; -- name: GetMemberData :many -SELECT username, email, user_id FROM user_account; +SELECT username, full_name, email, user_id FROM user_account; -- name: UpdateUserAccountInfo :one UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5 diff --git a/internal/db/user_accounts.sql.go b/internal/db/user_accounts.sql.go index 0a523dd..ee62982 100644 --- a/internal/db/user_accounts.sql.go +++ b/internal/db/user_accounts.sql.go @@ -102,11 +102,12 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) } const getMemberData = `-- name: GetMemberData :many -SELECT username, email, user_id FROM user_account +SELECT username, full_name, email, user_id FROM user_account ` type GetMemberDataRow struct { Username string `json:"username"` + FullName string `json:"full_name"` Email string `json:"email"` UserID uuid.UUID `json:"user_id"` } @@ -120,7 +121,12 @@ func (q *Queries) GetMemberData(ctx context.Context) ([]GetMemberDataRow, error) var items []GetMemberDataRow for rows.Next() { var i GetMemberDataRow - if err := rows.Scan(&i.Username, &i.Email, &i.UserID); err != nil { + if err := rows.Scan( + &i.Username, + &i.FullName, + &i.Email, + &i.UserID, + ); err != nil { return nil, err } items = append(items, i) diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 8c640f4..05a7795 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -65,11 +65,6 @@ type ComplexityRoot struct { Total func(childComplexity int) int } - CreateProjectMemberPayload struct { - Member func(childComplexity int) int - Ok func(childComplexity int) int - } - CreateTeamMemberPayload struct { Team func(childComplexity int) int TeamMember func(childComplexity int) int @@ -132,6 +127,11 @@ type ComplexityRoot struct { TaskGroup func(childComplexity int) int } + InviteProjectMemberPayload struct { + Member func(childComplexity int) int + Ok func(childComplexity int) int + } + LabelColor struct { ColorHex func(childComplexity int) int ID func(childComplexity int) int @@ -162,11 +162,10 @@ type ComplexityRoot struct { MemberSearchResult struct { Confirmed func(childComplexity int) int - FullName func(childComplexity int) int - ID func(childComplexity int) int + Invited func(childComplexity int) int Joined func(childComplexity int) int Similarity func(childComplexity int) int - Username func(childComplexity int) int + User func(childComplexity int) int } Mutation struct { @@ -175,7 +174,6 @@ type ComplexityRoot struct { ClearProfileAvatar func(childComplexity int) int CreateProject func(childComplexity int, input NewProject) int CreateProjectLabel func(childComplexity int, input NewProjectLabel) int - CreateProjectMember func(childComplexity int, input CreateProjectMember) int CreateRefreshToken func(childComplexity int, input NewRefreshToken) int CreateTask func(childComplexity int, input NewTask) int CreateTaskChecklist func(childComplexity int, input CreateTaskChecklist) int @@ -196,6 +194,7 @@ type ComplexityRoot struct { DeleteTeamMember func(childComplexity int, input DeleteTeamMember) int DeleteUserAccount func(childComplexity int, input DeleteUserAccount) int DuplicateTaskGroup func(childComplexity int, input DuplicateTaskGroup) int + InviteProjectMember func(childComplexity int, input InviteProjectMember) int LogoutUser func(childComplexity int, input LogoutUser) int RemoveTaskLabel func(childComplexity int, input *RemoveTaskLabelInput) int SetTaskChecklistItemComplete func(childComplexity int, input SetTaskChecklistItemComplete) int @@ -455,7 +454,7 @@ type MutationResolver interface { UpdateProjectLabel(ctx context.Context, input UpdateProjectLabel) (*db.ProjectLabel, error) UpdateProjectLabelName(ctx context.Context, input UpdateProjectLabelName) (*db.ProjectLabel, error) UpdateProjectLabelColor(ctx context.Context, input UpdateProjectLabelColor) (*db.ProjectLabel, error) - CreateProjectMember(ctx context.Context, input CreateProjectMember) (*CreateProjectMemberPayload, error) + InviteProjectMember(ctx context.Context, input InviteProjectMember) (*InviteProjectMemberPayload, error) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) UpdateProjectMemberRole(ctx context.Context, input UpdateProjectMemberRole) (*UpdateProjectMemberRolePayload, error) CreateTask(ctx context.Context, input NewTask) (*db.Task, error) @@ -620,20 +619,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ChecklistBadge.Total(childComplexity), true - case "CreateProjectMemberPayload.member": - if e.complexity.CreateProjectMemberPayload.Member == nil { - break - } - - return e.complexity.CreateProjectMemberPayload.Member(childComplexity), true - - case "CreateProjectMemberPayload.ok": - if e.complexity.CreateProjectMemberPayload.Ok == nil { - break - } - - return e.complexity.CreateProjectMemberPayload.Ok(childComplexity), true - case "CreateTeamMemberPayload.team": if e.complexity.CreateTeamMemberPayload.Team == nil { break @@ -816,6 +801,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DuplicateTaskGroupPayload.TaskGroup(childComplexity), true + case "InviteProjectMemberPayload.member": + if e.complexity.InviteProjectMemberPayload.Member == nil { + break + } + + return e.complexity.InviteProjectMemberPayload.Member(childComplexity), true + + case "InviteProjectMemberPayload.ok": + if e.complexity.InviteProjectMemberPayload.Ok == nil { + break + } + + return e.complexity.InviteProjectMemberPayload.Ok(childComplexity), true + case "LabelColor.colorHex": if e.complexity.LabelColor.ColorHex == nil { break @@ -935,19 +934,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.MemberSearchResult.Confirmed(childComplexity), true - case "MemberSearchResult.fullName": - if e.complexity.MemberSearchResult.FullName == nil { + case "MemberSearchResult.invited": + if e.complexity.MemberSearchResult.Invited == nil { break } - return e.complexity.MemberSearchResult.FullName(childComplexity), true - - case "MemberSearchResult.id": - if e.complexity.MemberSearchResult.ID == nil { - break - } - - return e.complexity.MemberSearchResult.ID(childComplexity), true + return e.complexity.MemberSearchResult.Invited(childComplexity), true case "MemberSearchResult.joined": if e.complexity.MemberSearchResult.Joined == nil { @@ -963,12 +955,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.MemberSearchResult.Similarity(childComplexity), true - case "MemberSearchResult.username": - if e.complexity.MemberSearchResult.Username == nil { + case "MemberSearchResult.user": + if e.complexity.MemberSearchResult.User == nil { break } - return e.complexity.MemberSearchResult.Username(childComplexity), true + return e.complexity.MemberSearchResult.User(childComplexity), true case "Mutation.addTaskLabel": if e.complexity.Mutation.AddTaskLabel == nil { @@ -1025,18 +1017,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateProjectLabel(childComplexity, args["input"].(NewProjectLabel)), true - case "Mutation.createProjectMember": - if e.complexity.Mutation.CreateProjectMember == nil { - break - } - - args, err := ec.field_Mutation_createProjectMember_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.CreateProjectMember(childComplexity, args["input"].(CreateProjectMember)), true - case "Mutation.createRefreshToken": if e.complexity.Mutation.CreateRefreshToken == nil { break @@ -1277,6 +1257,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DuplicateTaskGroup(childComplexity, args["input"].(DuplicateTaskGroup)), true + case "Mutation.inviteProjectMember": + if e.complexity.Mutation.InviteProjectMember == nil { + break + } + + args, err := ec.field_Mutation_inviteProjectMember_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.InviteProjectMember(childComplexity, args["input"].(InviteProjectMember)), true + case "Mutation.logoutUser": if e.complexity.Mutation.LogoutUser == nil { break @@ -2877,20 +2869,22 @@ input UpdateProjectLabelColor { } extend type Mutation { - createProjectMember(input: CreateProjectMember!): - CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + # TODO: rename to inviteProjectMember + inviteProjectMember(input: InviteProjectMember!): + InviteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) } -input CreateProjectMember { +input InviteProjectMember { projectID: UUID! - userID: UUID! + userID: UUID + email: String } -type CreateProjectMemberPayload { +type InviteProjectMemberPayload { ok: Boolean! member: Member! } @@ -3283,11 +3277,10 @@ input MemberSearchFilter { } type MemberSearchResult { - id: UUID! similarity: Int! - username: String! - fullName: String! + user: UserAccount! confirmed: Boolean! + invited: Boolean! joined: Boolean! } @@ -3428,20 +3421,6 @@ func (ec *executionContext) field_Mutation_createProjectLabel_args(ctx context.C return args, nil } -func (ec *executionContext) field_Mutation_createProjectMember_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 CreateProjectMember - if tmp, ok := rawArgs["input"]; ok { - arg0, err = ec.unmarshalNCreateProjectMember2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateProjectMember(ctx, tmp) - if err != nil { - return nil, err - } - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_createProject_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3736,6 +3715,20 @@ func (ec *executionContext) field_Mutation_duplicateTaskGroup_args(ctx context.C return args, nil } +func (ec *executionContext) field_Mutation_inviteProjectMember_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 InviteProjectMember + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNInviteProjectMember2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMember(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_logoutUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4302,74 +4295,6 @@ func (ec *executionContext) _ChecklistBadge_total(ctx context.Context, field gra return ec.marshalNInt2int(ctx, field.Selections, res) } -func (ec *executionContext) _CreateProjectMemberPayload_ok(ctx context.Context, field graphql.CollectedField, obj *CreateProjectMemberPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "CreateProjectMemberPayload", - 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.Ok, 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.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) -} - -func (ec *executionContext) _CreateProjectMemberPayload_member(ctx context.Context, field graphql.CollectedField, obj *CreateProjectMemberPayload) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "CreateProjectMemberPayload", - 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.(*Member) - fc.Result = res - return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) -} - func (ec *executionContext) _CreateTeamMemberPayload_team(ctx context.Context, field graphql.CollectedField, obj *CreateTeamMemberPayload) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5254,6 +5179,74 @@ func (ec *executionContext) _DuplicateTaskGroupPayload_taskGroup(ctx context.Con return ec.marshalNTaskGroup2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐTaskGroup(ctx, field.Selections, res) } +func (ec *executionContext) _InviteProjectMemberPayload_ok(ctx context.Context, field graphql.CollectedField, obj *InviteProjectMemberPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "InviteProjectMemberPayload", + 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.Ok, 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.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) _InviteProjectMemberPayload_member(ctx context.Context, field graphql.CollectedField, obj *InviteProjectMemberPayload) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "InviteProjectMemberPayload", + 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.(*Member) + fc.Result = res + return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) +} + func (ec *executionContext) _LabelColor_id(ctx context.Context, field graphql.CollectedField, obj *db.LabelColor) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5798,40 +5791,6 @@ func (ec *executionContext) _MemberList_projects(ctx context.Context, field grap return ec.marshalNProject2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐProjectᚄ(ctx, field.Selections, res) } -func (ec *executionContext) _MemberSearchResult_id(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "MemberSearchResult", - 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.ID, 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.(uuid.UUID) - fc.Result = res - return ec.marshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) -} - func (ec *executionContext) _MemberSearchResult_similarity(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5866,7 +5825,7 @@ func (ec *executionContext) _MemberSearchResult_similarity(ctx context.Context, return ec.marshalNInt2int(ctx, field.Selections, res) } -func (ec *executionContext) _MemberSearchResult_username(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { +func (ec *executionContext) _MemberSearchResult_user(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -5883,7 +5842,7 @@ func (ec *executionContext) _MemberSearchResult_username(ctx context.Context, fi 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.Username, nil + return obj.User, nil }) if err != nil { ec.Error(ctx, err) @@ -5895,43 +5854,9 @@ func (ec *executionContext) _MemberSearchResult_username(ctx context.Context, fi } return graphql.Null } - res := resTmp.(string) + res := resTmp.(*db.UserAccount) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) _MemberSearchResult_fullName(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "MemberSearchResult", - 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.FullName, 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.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res) } func (ec *executionContext) _MemberSearchResult_confirmed(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { @@ -5968,6 +5893,40 @@ func (ec *executionContext) _MemberSearchResult_confirmed(ctx context.Context, f return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _MemberSearchResult_invited(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "MemberSearchResult", + 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.Invited, 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.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _MemberSearchResult_joined(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6586,7 +6545,7 @@ func (ec *executionContext) _Mutation_updateProjectLabelColor(ctx context.Contex return ec.marshalNProjectLabel2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐProjectLabel(ctx, field.Selections, res) } -func (ec *executionContext) _Mutation_createProjectMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_inviteProjectMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -6602,7 +6561,7 @@ func (ec *executionContext) _Mutation_createProjectMember(ctx context.Context, f ctx = graphql.WithFieldContext(ctx, fc) rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Mutation_createProjectMember_args(ctx, rawArgs) + args, err := ec.field_Mutation_inviteProjectMember_args(ctx, rawArgs) if err != nil { ec.Error(ctx, err) return graphql.Null @@ -6611,7 +6570,7 @@ func (ec *executionContext) _Mutation_createProjectMember(ctx context.Context, f resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateProjectMember(rctx, args["input"].(CreateProjectMember)) + return ec.resolvers.Mutation().InviteProjectMember(rctx, args["input"].(InviteProjectMember)) } directive1 := func(ctx context.Context) (interface{}, error) { roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"}) @@ -6639,10 +6598,10 @@ func (ec *executionContext) _Mutation_createProjectMember(ctx context.Context, f if tmp == nil { return nil, nil } - if data, ok := tmp.(*CreateProjectMemberPayload); ok { + if data, ok := tmp.(*InviteProjectMemberPayload); ok { return data, nil } - return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.CreateProjectMemberPayload`, tmp) + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.InviteProjectMemberPayload`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6654,9 +6613,9 @@ func (ec *executionContext) _Mutation_createProjectMember(ctx context.Context, f } return graphql.Null } - res := resTmp.(*CreateProjectMemberPayload) + res := resTmp.(*InviteProjectMemberPayload) fc.Result = res - return ec.marshalNCreateProjectMemberPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateProjectMemberPayload(ctx, field.Selections, res) + return ec.marshalNInviteProjectMemberPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMemberPayload(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_deleteProjectMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -15030,30 +14989,6 @@ func (ec *executionContext) unmarshalInputAssignTaskInput(ctx context.Context, o return it, nil } -func (ec *executionContext) unmarshalInputCreateProjectMember(ctx context.Context, obj interface{}) (CreateProjectMember, error) { - var it CreateProjectMember - var asMap = obj.(map[string]interface{}) - - for k, v := range asMap { - switch k { - case "projectID": - var err error - it.ProjectID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) - if err != nil { - return it, err - } - case "userID": - var err error - it.UserID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) - if err != nil { - return it, err - } - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputCreateTaskChecklist(ctx context.Context, obj interface{}) (CreateTaskChecklist, error) { var it CreateTaskChecklist var asMap = obj.(map[string]interface{}) @@ -15468,6 +15403,36 @@ func (ec *executionContext) unmarshalInputFindUser(ctx context.Context, obj inte return it, nil } +func (ec *executionContext) unmarshalInputInviteProjectMember(ctx context.Context, obj interface{}) (InviteProjectMember, error) { + var it InviteProjectMember + var asMap = obj.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "projectID": + var err error + it.ProjectID, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "userID": + var err error + it.UserID, err = ec.unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v) + if err != nil { + return it, err + } + case "email": + var err error + it.Email, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputLogoutUser(ctx context.Context, obj interface{}) (LogoutUser, error) { var it LogoutUser var asMap = obj.(map[string]interface{}) @@ -16438,38 +16403,6 @@ func (ec *executionContext) _ChecklistBadge(ctx context.Context, sel ast.Selecti return out } -var createProjectMemberPayloadImplementors = []string{"CreateProjectMemberPayload"} - -func (ec *executionContext) _CreateProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, obj *CreateProjectMemberPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, createProjectMemberPayloadImplementors) - - out := graphql.NewFieldSet(fields) - var invalids uint32 - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("CreateProjectMemberPayload") - case "ok": - out.Values[i] = ec._CreateProjectMemberPayload_ok(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "member": - out.Values[i] = ec._CreateProjectMemberPayload_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 createTeamMemberPayloadImplementors = []string{"CreateTeamMemberPayload"} func (ec *executionContext) _CreateTeamMemberPayload(ctx context.Context, sel ast.SelectionSet, obj *CreateTeamMemberPayload) graphql.Marshaler { @@ -16864,6 +16797,38 @@ func (ec *executionContext) _DuplicateTaskGroupPayload(ctx context.Context, sel return out } +var inviteProjectMemberPayloadImplementors = []string{"InviteProjectMemberPayload"} + +func (ec *executionContext) _InviteProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, obj *InviteProjectMemberPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, inviteProjectMemberPayloadImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("InviteProjectMemberPayload") + case "ok": + out.Values[i] = ec._InviteProjectMemberPayload_ok(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "member": + out.Values[i] = ec._InviteProjectMemberPayload_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 labelColorImplementors = []string{"LabelColor"} func (ec *executionContext) _LabelColor(ctx context.Context, sel ast.SelectionSet, obj *db.LabelColor) graphql.Marshaler { @@ -17052,23 +17017,13 @@ func (ec *executionContext) _MemberSearchResult(ctx context.Context, sel ast.Sel switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("MemberSearchResult") - case "id": - out.Values[i] = ec._MemberSearchResult_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } case "similarity": out.Values[i] = ec._MemberSearchResult_similarity(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } - case "username": - out.Values[i] = ec._MemberSearchResult_username(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "fullName": - out.Values[i] = ec._MemberSearchResult_fullName(ctx, field, obj) + case "user": + out.Values[i] = ec._MemberSearchResult_user(ctx, field, obj) if out.Values[i] == graphql.Null { invalids++ } @@ -17077,6 +17032,11 @@ func (ec *executionContext) _MemberSearchResult(ctx context.Context, sel ast.Sel if out.Values[i] == graphql.Null { invalids++ } + case "invited": + out.Values[i] = ec._MemberSearchResult_invited(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "joined": out.Values[i] = ec._MemberSearchResult_joined(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -17148,8 +17108,8 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } - case "createProjectMember": - out.Values[i] = ec._Mutation_createProjectMember(ctx, field) + case "inviteProjectMember": + out.Values[i] = ec._Mutation_inviteProjectMember(ctx, field) if out.Values[i] == graphql.Null { invalids++ } @@ -19437,24 +19397,6 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } -func (ec *executionContext) unmarshalNCreateProjectMember2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateProjectMember(ctx context.Context, v interface{}) (CreateProjectMember, error) { - return ec.unmarshalInputCreateProjectMember(ctx, v) -} - -func (ec *executionContext) marshalNCreateProjectMemberPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, v CreateProjectMemberPayload) graphql.Marshaler { - return ec._CreateProjectMemberPayload(ctx, sel, &v) -} - -func (ec *executionContext) marshalNCreateProjectMemberPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, v *CreateProjectMemberPayload) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - return ec._CreateProjectMemberPayload(ctx, sel, v) -} - func (ec *executionContext) unmarshalNCreateTaskChecklist2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐCreateTaskChecklist(ctx context.Context, v interface{}) (CreateTaskChecklist, error) { return ec.unmarshalInputCreateTaskChecklist(ctx, v) } @@ -19750,6 +19692,24 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) unmarshalNInviteProjectMember2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMember(ctx context.Context, v interface{}) (InviteProjectMember, error) { + return ec.unmarshalInputInviteProjectMember(ctx, v) +} + +func (ec *executionContext) marshalNInviteProjectMemberPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, v InviteProjectMemberPayload) graphql.Marshaler { + return ec._InviteProjectMemberPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNInviteProjectMemberPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐInviteProjectMemberPayload(ctx context.Context, sel ast.SelectionSet, v *InviteProjectMemberPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._InviteProjectMemberPayload(ctx, sel, v) +} + func (ec *executionContext) marshalNLabelColor2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐLabelColor(ctx context.Context, sel ast.SelectionSet, v db.LabelColor) graphql.Marshaler { return ec._LabelColor(ctx, sel, &v) } diff --git a/internal/graph/models_gen.go b/internal/graph/models_gen.go index 1e89966..271b584 100644 --- a/internal/graph/models_gen.go +++ b/internal/graph/models_gen.go @@ -27,16 +27,6 @@ type ChecklistBadge struct { Total int `json:"total"` } -type CreateProjectMember struct { - ProjectID uuid.UUID `json:"projectID"` - UserID uuid.UUID `json:"userID"` -} - -type CreateProjectMemberPayload struct { - Ok bool `json:"ok"` - Member *Member `json:"member"` -} - type CreateTaskChecklist struct { TaskID uuid.UUID `json:"taskID"` Name string `json:"name"` @@ -187,6 +177,17 @@ type FindUser struct { UserID uuid.UUID `json:"userID"` } +type InviteProjectMember struct { + ProjectID uuid.UUID `json:"projectID"` + UserID *uuid.UUID `json:"userID"` + Email *string `json:"email"` +} + +type InviteProjectMemberPayload struct { + Ok bool `json:"ok"` + Member *Member `json:"member"` +} + type LogoutUser struct { UserID uuid.UUID `json:"userID"` } @@ -218,12 +219,11 @@ type MemberSearchFilter struct { } type MemberSearchResult struct { - ID uuid.UUID `json:"id"` - Similarity int `json:"similarity"` - Username string `json:"username"` - FullName string `json:"fullName"` - Confirmed bool `json:"confirmed"` - Joined bool `json:"joined"` + Similarity int `json:"similarity"` + User *db.UserAccount `json:"user"` + Confirmed bool `json:"confirmed"` + Invited bool `json:"invited"` + Joined bool `json:"joined"` } type NewProject struct { diff --git a/internal/graph/schema.graphqls b/internal/graph/schema.graphqls index e56df79..1ef322e 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/schema.graphqls @@ -338,20 +338,22 @@ input UpdateProjectLabelColor { } extend type Mutation { - createProjectMember(input: CreateProjectMember!): - CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + # TODO: rename to inviteProjectMember + inviteProjectMember(input: InviteProjectMember!): + InviteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) } -input CreateProjectMember { +input InviteProjectMember { projectID: UUID! - userID: UUID! + userID: UUID + email: String } -type CreateProjectMemberPayload { +type InviteProjectMemberPayload { ok: Boolean! member: Member! } @@ -744,11 +746,10 @@ input MemberSearchFilter { } type MemberSearchResult { - id: UUID! similarity: Int! - username: String! - fullName: String! + user: UserAccount! confirmed: Boolean! + invited: Boolean! joined: Boolean! } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index f88ff19..f705435 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -124,33 +124,53 @@ func (r *mutationResolver) UpdateProjectLabelColor(ctx context.Context, input Up return &label, err } -func (r *mutationResolver) CreateProjectMember(ctx context.Context, input CreateProjectMember) (*CreateProjectMemberPayload, error) { - addedAt := time.Now().UTC() - _, err := r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID, UserID: input.UserID, AddedAt: addedAt, RoleCode: "member"}) - if err != nil { - return &CreateProjectMemberPayload{Ok: false}, err +func (r *mutationResolver) InviteProjectMember(ctx context.Context, input InviteProjectMember) (*InviteProjectMemberPayload, error) { + if input.Email != nil && input.UserID != nil { + return &InviteProjectMemberPayload{Ok: false}, &gqlerror.Error{ + Message: "Both email and userID can not be used to invite a project member", + Extensions: map[string]interface{}{ + "code": "403", + }, + } + } else if input.Email == nil && input.UserID == nil { + return &InviteProjectMemberPayload{Ok: false}, &gqlerror.Error{ + Message: "Either email or userID must be set to invite a project member", + Extensions: map[string]interface{}{ + "code": "403", + }, + } } - user, err := r.Repository.GetUserAccountByID(ctx, input.UserID) - if err != nil { - return &CreateProjectMemberPayload{Ok: false}, err - } - var url *string - if user.ProfileAvatarUrl.Valid { - url = &user.ProfileAvatarUrl.String - } - profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor} + if input.UserID != nil { + addedAt := time.Now().UTC() + _, err := r.Repository.CreateProjectMember(ctx, db.CreateProjectMemberParams{ProjectID: input.ProjectID, UserID: *input.UserID, AddedAt: addedAt, RoleCode: "member"}) + if err != nil { + return &InviteProjectMemberPayload{Ok: false}, err + } + user, err := r.Repository.GetUserAccountByID(ctx, *input.UserID) + if err != nil && err != sql.ErrNoRows { + return &InviteProjectMemberPayload{Ok: false}, err - role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: input.UserID, ProjectID: input.ProjectID}) - if err != nil { - return &CreateProjectMemberPayload{Ok: false}, err + } + var url *string + if user.ProfileAvatarUrl.Valid { + url = &user.ProfileAvatarUrl.String + } + profileIcon := &ProfileIcon{url, &user.Initials, &user.ProfileBgColor} + + role, err := r.Repository.GetRoleForProjectMemberByUserID(ctx, db.GetRoleForProjectMemberByUserIDParams{UserID: *input.UserID, ProjectID: input.ProjectID}) + if err != nil { + return &InviteProjectMemberPayload{Ok: false}, err + } + return &InviteProjectMemberPayload{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 } - 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 + // invite user + return &InviteProjectMemberPayload{Ok: false}, errors.New("not implemented") } func (r *mutationResolver) DeleteProjectMember(ctx context.Context, input DeleteProjectMember) (*DeleteProjectMemberPayload, error) { @@ -1222,7 +1242,7 @@ func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFil } return []MemberSearchResult{}, err } - results = append(results, MemberSearchResult{FullName: user.FullName, Username: user.Username, Joined: false, Confirmed: false, Similarity: rank.Distance, ID: user.UserID}) + results = append(results, MemberSearchResult{User: &user, Joined: false, Confirmed: false, Similarity: rank.Distance}) memberList[masterList[rank.Target]] = true } } diff --git a/internal/graph/schema/project_member.gql b/internal/graph/schema/project_member.gql index 5c22b36..030ae62 100644 --- a/internal/graph/schema/project_member.gql +++ b/internal/graph/schema/project_member.gql @@ -1,18 +1,20 @@ extend type Mutation { - createProjectMember(input: CreateProjectMember!): - CreateProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) + # TODO: rename to inviteProjectMember + inviteProjectMember(input: InviteProjectMember!): + InviteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) deleteProjectMember(input: DeleteProjectMember!): DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) updateProjectMemberRole(input: UpdateProjectMemberRole!): UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) } -input CreateProjectMember { +input InviteProjectMember { projectID: UUID! - userID: UUID! + userID: UUID + email: String } -type CreateProjectMemberPayload { +type InviteProjectMemberPayload { ok: Boolean! member: Member! } diff --git a/internal/graph/schema/user.gql b/internal/graph/schema/user.gql index 5784d90..16c5103 100644 --- a/internal/graph/schema/user.gql +++ b/internal/graph/schema/user.gql @@ -25,11 +25,10 @@ input MemberSearchFilter { } type MemberSearchResult { - id: UUID! similarity: Int! - username: String! - fullName: String! + user: UserAccount! confirmed: Boolean! + invited: Boolean! joined: Boolean! }