feat: add public projects

This commit is contained in:
Jordan Knott 2020-09-21 21:43:56 -05:00
parent 696a9aeee7
commit 36f25391b4
14 changed files with 714 additions and 23 deletions

View File

@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect, useContext } from 'react';
import updateApolloCache from 'shared/utils/cache'; import updateApolloCache from 'shared/utils/cache';
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar'; import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar';
import styled from 'styled-components/macro'; import styled from 'styled-components/macro';
import AsyncSelect from 'react-select/async';
import { usePopup, Popup } from 'shared/components/PopupMenu'; import { usePopup, Popup } from 'shared/components/PopupMenu';
import { import {
useParams, useParams,
@ -37,12 +38,18 @@ import Input from 'shared/components/Input';
import Member from 'shared/components/Member'; import Member from 'shared/components/Member';
import EmptyBoard from 'shared/components/EmptyBoard'; import EmptyBoard from 'shared/components/EmptyBoard';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { Lock } from 'shared/icons';
import Button from 'shared/components/Button';
import { useApolloClient } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import Board, { BoardLoading } from './Board'; import Board, { BoardLoading } from './Board';
import Details from './Details'; import Details from './Details';
import LabelManagerEditor from './LabelManagerEditor'; import LabelManagerEditor from './LabelManagerEditor';
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant'; const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
const RFC2822_EMAIL = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => { const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || ''); const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
@ -76,23 +83,90 @@ type UserManagementPopupProps = {
onAddProjectMember: (userID: string) => void; onAddProjectMember: (userID: string) => void;
}; };
const VisibiltyPrivateIcon = styled(Lock)`
padding-right: 4px;
`;
const VisibiltyButtonText = styled.span`
color: rgba(${props => props.theme.colors.text.primary});
`;
const ShareActions = styled.div`
border-top: 1px solid #414561;
margin-top: 8px;
padding-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
`;
const VisibiltyButton = styled.button`
cursor: pointer;
margin: 2px 4px;
padding: 2px 4px;
align-items: center;
justify-content: center;
border-bottom: 1px solid transparent;
&:hover ${VisibiltyButtonText} {
color: rgba(${props => props.theme.colors.text.secondary});
}
&:hover ${VisibiltyPrivateIcon} {
fill: rgba(${props => props.theme.colors.text.secondary});
stroke: rgba(${props => props.theme.colors.text.secondary});
}
&:hover {
border-bottom: 1px solid rgba(${props => props.theme.colors.primary});
}
`;
type MemberFilterOptions = {
projectID?: null | string;
teamID?: null | string;
organization?: boolean;
};
const fetchMembers = async (client: any, options: MemberFilterOptions, input: string, cb: any) => {
if (input && input.trim().length < 3) {
return [];
}
const res = await client.query({
query: gql`
query {
searchMembers(input: {SearchFilter:"${input}"}) {
id
similarity
username
fullName
confirmed
joined
}
}
`,
});
let results: any = [];
if (res.data && res.data.searchMembers) {
results = [...res.data.searchMembers.map((m: any) => ({ label: m.fullName, value: m.id }))];
}
if (RFC2822_EMAIL.test(input)) {
results = [...results, { label: input, value: input }];
}
return results;
};
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, projectMembers, onAddProjectMember }) => { const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, projectMembers, onAddProjectMember }) => {
const client = useApolloClient();
return ( return (
<Popup tab={0} title="Invite a user"> <Popup tab={0} title="Invite a user">
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" /> <AsyncSelect isMulti cacheOptions defaultOption loadOptions={(i, cb) => fetchMembers(client, {}, i, cb)} />
<MemberList> <ShareActions>
{users <VisibiltyButton>
.filter(u => u.id !== projectMembers.find(p => p.id === u.id)?.id) <VisibiltyPrivateIcon width={12} height={12} />
.map(user => ( <VisibiltyButtonText>Private</VisibiltyButtonText>
<UserMember </VisibiltyButton>
key={user.id} </ShareActions>
onCardMemberClick={() => onAddProjectMember(user.id)}
showName
member={user}
taskID=""
/>
))}
</MemberList>
</Popup> </Popup>
); );
}; };

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const EyeSlash: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 640 512">
<path d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z" />
</Icon>
);
};
export default EyeSlash;

View File

@ -1,6 +1,7 @@
import Cross from './Cross'; import Cross from './Cross';
import Cog from './Cog'; import Cog from './Cog';
import Eye from './Eye'; import Eye from './Eye';
import EyeSlash from './EyeSlash';
import List from './List'; import List from './List';
import At from './At'; import At from './At';
import Task from './Task'; import Task from './Task';
@ -88,5 +89,6 @@ export {
Paperclip, Paperclip,
Share, Share,
Eye, Eye,
EyeSlash,
List, List,
}; };

1
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/jmoiron/sqlx v1.2.0 github.com/jmoiron/sqlx v1.2.0
github.com/lib/pq v1.3.0 github.com/lib/pq v1.3.0
github.com/lithammer/fuzzysearch v1.1.0
github.com/magefile/mage v1.9.0 github.com/magefile/mage v1.9.0
github.com/pelletier/go-toml v1.8.0 // indirect github.com/pelletier/go-toml v1.8.0 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1

2
go.sum
View File

@ -357,6 +357,8 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A=
github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE= github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=

View File

@ -122,12 +122,12 @@ func (q *Queries) DeleteProjectMember(ctx context.Context, arg DeleteProjectMemb
return err return err
} }
const getAllProjects = `-- name: GetAllProjects :many const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many
SELECT project_id, team_id, created_at, name FROM project SELECT project_id, team_id, created_at, name FROM project WHERE team_id = $1
` `
func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) { func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) {
rows, err := q.db.QueryContext(ctx, getAllProjects) rows, err := q.db.QueryContext(ctx, getAllProjectsForTeam, teamID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -154,12 +154,12 @@ func (q *Queries) GetAllProjects(ctx context.Context) ([]Project, error) {
return items, nil return items, nil
} }
const getAllProjectsForTeam = `-- name: GetAllProjectsForTeam :many const getAllTeamProjects = `-- name: GetAllTeamProjects :many
SELECT project_id, team_id, created_at, name FROM project WHERE team_id = $1 SELECT project_id, team_id, created_at, name FROM project WHERE team_id IS NOT null
` `
func (q *Queries) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) { func (q *Queries) GetAllTeamProjects(ctx context.Context) ([]Project, error) {
rows, err := q.db.QueryContext(ctx, getAllProjectsForTeam, teamID) rows, err := q.db.QueryContext(ctx, getAllTeamProjects)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -49,10 +49,10 @@ type Querier interface {
DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error) GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
GetAllOrganizations(ctx context.Context) ([]Organization, error) GetAllOrganizations(ctx context.Context) ([]Organization, error)
GetAllProjects(ctx context.Context) ([]Project, error)
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error) GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
GetAllTaskGroups(ctx context.Context) ([]TaskGroup, error) GetAllTaskGroups(ctx context.Context) ([]TaskGroup, error)
GetAllTasks(ctx context.Context) ([]Task, error) GetAllTasks(ctx context.Context) ([]Task, error)
GetAllTeamProjects(ctx context.Context) ([]Project, error)
GetAllTeams(ctx context.Context) ([]Team, error) GetAllTeams(ctx context.Context) ([]Team, error)
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error) GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
@ -61,6 +61,7 @@ type Querier interface {
GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error) GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error)
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error) GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
GetLabelColors(ctx context.Context) ([]LabelColor, error) GetLabelColors(ctx context.Context) ([]LabelColor, error)
GetMemberData(ctx context.Context) ([]GetMemberDataRow, error)
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error) GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error)

View File

@ -15,6 +15,9 @@ INSERT INTO user_account(full_name, initials, email, username, created_at, passw
UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1 UPDATE user_account SET profile_avatar_url = $2 WHERE user_id = $1
RETURNING *; RETURNING *;
-- name: GetMemberData :many
SELECT username, email, user_id FROM user_account;
-- name: UpdateUserAccountInfo :one -- name: UpdateUserAccountInfo :one
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5 UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
WHERE user_id = $1 RETURNING *; WHERE user_id = $1 RETURNING *;

View File

@ -101,6 +101,39 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
return items, nil return items, nil
} }
const getMemberData = `-- name: GetMemberData :many
SELECT username, email, user_id FROM user_account
`
type GetMemberDataRow struct {
Username string `json:"username"`
Email string `json:"email"`
UserID uuid.UUID `json:"user_id"`
}
func (q *Queries) GetMemberData(ctx context.Context) ([]GetMemberDataRow, error) {
rows, err := q.db.QueryContext(ctx, getMemberData)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetMemberDataRow
for rows.Next() {
var i GetMemberDataRow
if err := rows.Scan(&i.Username, &i.Email, &i.UserID); 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 getRoleForUserID = `-- name: GetRoleForUserID :one const getRoleForUserID = `-- name: GetRoleForUserID :one
SELECT username, role.code, role.name FROM user_account SELECT username, role.code, role.name FROM user_account
INNER JOIN role ON role.code = user_account.role_code INNER JOIN role ON role.code = user_account.role_code

View File

@ -160,6 +160,15 @@ type ComplexityRoot struct {
Teams func(childComplexity int) int Teams func(childComplexity int) int
} }
MemberSearchResult struct {
Confirmed func(childComplexity int) int
FullName func(childComplexity int) int
ID func(childComplexity int) int
Joined func(childComplexity int) int
Similarity func(childComplexity int) int
Username func(childComplexity int) int
}
Mutation struct { Mutation struct {
AddTaskLabel func(childComplexity int, input *AddTaskLabelInput) int AddTaskLabel func(childComplexity int, input *AddTaskLabelInput) int
AssignTask func(childComplexity int, input *AssignTaskInput) int AssignTask func(childComplexity int, input *AssignTaskInput) int
@ -289,6 +298,7 @@ type ComplexityRoot struct {
Notifications func(childComplexity int) int Notifications func(childComplexity int) int
Organizations func(childComplexity int) int Organizations func(childComplexity int) int
Projects func(childComplexity int, input *ProjectsFilter) int Projects func(childComplexity int, input *ProjectsFilter) int
SearchMembers func(childComplexity int, input MemberSearchFilter) int
TaskGroups func(childComplexity int) int TaskGroups func(childComplexity int) int
Teams func(childComplexity int) int Teams func(childComplexity int) int
Users func(childComplexity int) int Users func(childComplexity int) int
@ -528,6 +538,7 @@ type QueryResolver interface {
TaskGroups(ctx context.Context) ([]db.TaskGroup, error) TaskGroups(ctx context.Context) ([]db.TaskGroup, error)
Me(ctx context.Context) (*MePayload, error) Me(ctx context.Context) (*MePayload, error)
Notifications(ctx context.Context) ([]db.Notification, error) Notifications(ctx context.Context) ([]db.Notification, error)
SearchMembers(ctx context.Context, input MemberSearchFilter) ([]MemberSearchResult, error)
} }
type RefreshTokenResolver interface { type RefreshTokenResolver interface {
ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error)
@ -917,6 +928,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.MemberList.Teams(childComplexity), true return e.complexity.MemberList.Teams(childComplexity), true
case "MemberSearchResult.confirmed":
if e.complexity.MemberSearchResult.Confirmed == nil {
break
}
return e.complexity.MemberSearchResult.Confirmed(childComplexity), true
case "MemberSearchResult.fullName":
if e.complexity.MemberSearchResult.FullName == 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
case "MemberSearchResult.joined":
if e.complexity.MemberSearchResult.Joined == nil {
break
}
return e.complexity.MemberSearchResult.Joined(childComplexity), true
case "MemberSearchResult.similarity":
if e.complexity.MemberSearchResult.Similarity == nil {
break
}
return e.complexity.MemberSearchResult.Similarity(childComplexity), true
case "MemberSearchResult.username":
if e.complexity.MemberSearchResult.Username == nil {
break
}
return e.complexity.MemberSearchResult.Username(childComplexity), true
case "Mutation.addTaskLabel": case "Mutation.addTaskLabel":
if e.complexity.Mutation.AddTaskLabel == nil { if e.complexity.Mutation.AddTaskLabel == nil {
break break
@ -1862,6 +1915,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.Projects(childComplexity, args["input"].(*ProjectsFilter)), true return e.complexity.Query.Projects(childComplexity, args["input"].(*ProjectsFilter)), true
case "Query.searchMembers":
if e.complexity.Query.SearchMembers == nil {
break
}
args, err := ec.field_Query_searchMembers_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.SearchMembers(childComplexity, args["input"].(MemberSearchFilter)), true
case "Query.taskGroups": case "Query.taskGroups":
if e.complexity.Query.TaskGroups == nil { if e.complexity.Query.TaskGroups == nil {
break break
@ -3208,6 +3273,24 @@ extend type Mutation {
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG) UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
} }
extend type Query {
searchMembers(input: MemberSearchFilter!): [MemberSearchResult!]!
}
input MemberSearchFilter {
SearchFilter: String!
projectID: UUID
}
type MemberSearchResult {
id: UUID!
similarity: Int!
username: String!
fullName: String!
confirmed: Boolean!
joined: Boolean!
}
type UpdateUserInfoPayload { type UpdateUserInfoPayload {
user: UserAccount! user: UserAccount!
} }
@ -4101,6 +4184,20 @@ func (ec *executionContext) field_Query_projects_args(ctx context.Context, rawAr
return args, nil return args, nil
} }
func (ec *executionContext) field_Query_searchMembers_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 MemberSearchFilter
if tmp, ok := rawArgs["input"]; ok {
arg0, err = ec.unmarshalNMemberSearchFilter2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberSearchFilter(ctx, tmp)
if err != nil {
return nil, err
}
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -5701,6 +5798,210 @@ func (ec *executionContext) _MemberList_projects(ctx context.Context, field grap
return ec.marshalNProject2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐProjectᚄ(ctx, field.Selections, res) 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 {
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.Similarity, 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.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _MemberSearchResult_username(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.Username, 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)
}
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)
}
func (ec *executionContext) _MemberSearchResult_confirmed(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.Confirmed, 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 {
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.Joined, 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) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -10978,6 +11279,47 @@ func (ec *executionContext) _Query_notifications(ctx context.Context, field grap
return ec.marshalNNotification2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐNotificationᚄ(ctx, field.Selections, res) return ec.marshalNNotification2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐNotificationᚄ(ctx, field.Selections, res)
} }
func (ec *executionContext) _Query_searchMembers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Query",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Query_searchMembers_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().SearchMembers(rctx, args["input"].(MemberSearchFilter))
})
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.([]MemberSearchResult)
fc.Result = res
return ec.marshalNMemberSearchResult2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberSearchResultᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -15144,6 +15486,30 @@ func (ec *executionContext) unmarshalInputLogoutUser(ctx context.Context, obj in
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputMemberSearchFilter(ctx context.Context, obj interface{}) (MemberSearchFilter, error) {
var it MemberSearchFilter
var asMap = obj.(map[string]interface{})
for k, v := range asMap {
switch k {
case "SearchFilter":
var err error
it.SearchFilter, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
case "projectID":
var err error
it.ProjectID, err = ec.unmarshalOUUID2ᚖgithubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputNewProject(ctx context.Context, obj interface{}) (NewProject, error) { func (ec *executionContext) unmarshalInputNewProject(ctx context.Context, obj interface{}) (NewProject, error) {
var it NewProject var it NewProject
var asMap = obj.(map[string]interface{}) var asMap = obj.(map[string]interface{})
@ -16675,6 +17041,58 @@ func (ec *executionContext) _MemberList(ctx context.Context, sel ast.SelectionSe
return out return out
} }
var memberSearchResultImplementors = []string{"MemberSearchResult"}
func (ec *executionContext) _MemberSearchResult(ctx context.Context, sel ast.SelectionSet, obj *MemberSearchResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, memberSearchResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
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)
if out.Values[i] == graphql.Null {
invalids++
}
case "confirmed":
out.Values[i] = ec._MemberSearchResult_confirmed(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 {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var mutationImplementors = []string{"Mutation"} var mutationImplementors = []string{"Mutation"}
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@ -17645,6 +18063,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
} }
return res return res
}) })
case "searchMembers":
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._Query_searchMembers(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "__type": case "__type":
out.Values[i] = ec._Query___type(ctx, field) out.Values[i] = ec._Query___type(ctx, field)
case "__schema": case "__schema":
@ -19452,6 +19884,51 @@ func (ec *executionContext) marshalNMemberList2ᚖgithubᚗcomᚋjordanknottᚋt
return ec._MemberList(ctx, sel, v) return ec._MemberList(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNMemberSearchFilter2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberSearchFilter(ctx context.Context, v interface{}) (MemberSearchFilter, error) {
return ec.unmarshalInputMemberSearchFilter(ctx, v)
}
func (ec *executionContext) marshalNMemberSearchResult2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberSearchResult(ctx context.Context, sel ast.SelectionSet, v MemberSearchResult) graphql.Marshaler {
return ec._MemberSearchResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNMemberSearchResult2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberSearchResultᚄ(ctx context.Context, sel ast.SelectionSet, v []MemberSearchResult) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNMemberSearchResult2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMemberSearchResult(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) unmarshalNNewProject2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐNewProject(ctx context.Context, v interface{}) (NewProject, error) { func (ec *executionContext) unmarshalNNewProject2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐNewProject(ctx context.Context, v interface{}) (NewProject, error) {
return ec.unmarshalInputNewProject(ctx, v) return ec.unmarshalInputNewProject(ctx, v)
} }

View File

@ -212,6 +212,20 @@ type MemberList struct {
Projects []db.Project `json:"projects"` Projects []db.Project `json:"projects"`
} }
type MemberSearchFilter struct {
SearchFilter string `json:"SearchFilter"`
ProjectID *uuid.UUID `json:"projectID"`
}
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"`
}
type NewProject struct { type NewProject struct {
TeamID *uuid.UUID `json:"teamID"` TeamID *uuid.UUID `json:"teamID"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -734,6 +734,24 @@ extend type Mutation {
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG) UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
} }
extend type Query {
searchMembers(input: MemberSearchFilter!): [MemberSearchResult!]!
}
input MemberSearchFilter {
SearchFilter: String!
projectID: UUID
}
type MemberSearchResult {
id: UUID!
similarity: Int!
username: String!
fullName: String!
confirmed: Boolean!
joined: Boolean!
}
type UpdateUserInfoPayload { type UpdateUserInfoPayload {
user: UserAccount! user: UserAccount!
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/auth" "github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/lithammer/fuzzysearch/fuzzy"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -1193,6 +1194,41 @@ func (r *queryResolver) Notifications(ctx context.Context) ([]db.Notification, e
return notifications, nil return notifications, nil
} }
func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFilter) ([]MemberSearchResult, error) {
availableMembers, err := r.Repository.GetMemberData(ctx)
if err != nil {
return []MemberSearchResult{}, err
}
sortList := []string{}
masterList := map[string]uuid.UUID{}
for _, member := range availableMembers {
sortList = append(sortList, member.Username)
sortList = append(sortList, member.Email)
masterList[member.Username] = member.UserID
masterList[member.Email] = member.UserID
}
rankedList := fuzzy.RankFind(input.SearchFilter, sortList)
results := []MemberSearchResult{}
memberList := map[uuid.UUID]bool{}
for _, rank := range rankedList {
if _, ok := memberList[masterList[rank.Target]]; !ok {
log.WithFields(log.Fields{"source": rank.Source, "target": rank.Target}).Info("searching")
userID := masterList[rank.Target]
user, err := r.Repository.GetUserAccountByID(ctx, userID)
if err != nil {
if err == sql.ErrNoRows {
continue
}
return []MemberSearchResult{}, err
}
results = append(results, MemberSearchResult{FullName: user.FullName, Username: user.Username, Joined: false, Confirmed: false, Similarity: rank.Distance, ID: user.UserID})
memberList[masterList[rank.Target]] = true
}
}
return results, nil
}
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) { func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
return obj.TokenID, nil return obj.TokenID, nil
} }

View File

@ -15,6 +15,24 @@ extend type Mutation {
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG) UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
} }
extend type Query {
searchMembers(input: MemberSearchFilter!): [MemberSearchResult!]!
}
input MemberSearchFilter {
SearchFilter: String!
projectID: UUID
}
type MemberSearchResult {
id: UUID!
similarity: Int!
username: String!
fullName: String!
confirmed: Boolean!
joined: Boolean!
}
type UpdateUserInfoPayload { type UpdateUserInfoPayload {
user: UserAccount! user: UserAccount!
} }