13 Commits

40 changed files with 806 additions and 118 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Task sorting & filtering - Task sorting & filtering
- Redesigned the Task Details UI - Redesigned the Task Details UI
- Implement task group actions (duplicate/delete all tasks/sort)
### Fixed ### Fixed
- removed CORS middleware to fix security issue - removed CORS middleware to fix security issue

View File

@ -21,6 +21,8 @@ Was this project useful? Please consider <a href="https://www.buymeacoffee.com/j
**Please note that this project is still in active development. Some options may not work yet! For updates on development, join the Discord server** **Please note that this project is still in active development. Some options may not work yet! For updates on development, join the Discord server**
![Taskcafe](./.github/taskcafe_preview.png)
## Features ## Features
Currently Taskcafe only offers basic task tracking through a Kanban board. Currently Taskcafe only offers basic task tracking through a Kanban board.

View File

@ -6,14 +6,6 @@
"@apollo/client": "^3.0.0-rc.8", "@apollo/client": "^3.0.0-rc.8",
"@apollo/react-common": "^3.1.4", "@apollo/react-common": "^3.1.4",
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^3.1.3",
"@fortawesome/fontawesome-svg-core": "^1.2.27",
"@fortawesome/free-brands-svg-icons": "^5.12.1",
"@fortawesome/free-regular-svg-icons": "^5.12.1",
"@fortawesome/free-solid-svg-icons": "^5.12.1",
"@fortawesome/react-fontawesome": "^0.1.8",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/date-fns": "^2.6.0", "@types/date-fns": "^2.6.0",

View File

@ -171,7 +171,7 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
const AdminRoute = () => { const AdminRoute = () => {
useEffect(() => { useEffect(() => {
document.title = 'Taskcafé | Admin'; document.title = 'Admin | Taskcafé';
}, []); }, []);
const { loading, data } = useUsersQuery(); const { loading, data } = useUsersQuery();
const { showPopup, hidePopup } = usePopup(); const { showPopup, hidePopup } = usePopup();

View File

@ -3,11 +3,20 @@ import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar'; import GlobalTopNavbar from 'App/TopNavbar';
import { getAccessToken } from 'shared/utils/accessToken'; import { getAccessToken } from 'shared/utils/accessToken';
import Settings from 'shared/components/Settings'; import Settings from 'shared/components/Settings';
import { useMeQuery, useClearProfileAvatarMutation, useUpdateUserPasswordMutation } from 'shared/generated/graphql'; import {
useMeQuery,
useClearProfileAvatarMutation,
useUpdateUserPasswordMutation,
useUpdateUserInfoMutation,
MeQuery,
MeDocument,
} from 'shared/generated/graphql';
import axios from 'axios'; import axios from 'axios';
import { useCurrentUser } from 'App/context'; import { useCurrentUser } from 'App/context';
import NOOP from 'shared/utils/noop'; import NOOP from 'shared/utils/noop';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import updateApolloCache from 'shared/utils/cache';
import produce from 'immer';
const MainContent = styled.div` const MainContent = styled.div`
padding: 0 0 50px 80px; padding: 0 0 50px 80px;
@ -19,6 +28,7 @@ const Projects = () => {
const $fileUpload = useRef<HTMLInputElement>(null); const $fileUpload = useRef<HTMLInputElement>(null);
const [clearProfileAvatar] = useClearProfileAvatarMutation(); const [clearProfileAvatar] = useClearProfileAvatarMutation();
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const [updateUserInfo] = useUpdateUserInfoMutation();
const [updateUserPassword] = useUpdateUserPasswordMutation(); const [updateUserPassword] = useUpdateUserPasswordMutation();
const { loading, data, refetch } = useMeQuery(); const { loading, data, refetch } = useMeQuery();
useEffect(() => { useEffect(() => {
@ -69,6 +79,13 @@ const Projects = () => {
toast('Password was changed!'); toast('Password was changed!');
done(); done();
}} }}
onChangeUserInfo={(d, done) => {
updateUserInfo({
variables: { name: d.full_name, bio: d.bio, email: d.email, initials: d.initials },
});
toast('User info was saved!');
done();
}}
onProfileAvatarRemove={() => { onProfileAvatarRemove={() => {
clearProfileAvatar(); clearProfileAvatar();
}} }}

View File

@ -260,11 +260,7 @@ const Projects = () => {
}, },
}); });
if (loading) { if (loading) {
return ( return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
<>
<span>loading</span>
</>
);
} }
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f']; const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];

View File

@ -85,18 +85,30 @@ type TeamsRouteProps = {
const Teams = () => { const Teams = () => {
const { teamID } = useParams<TeamsRouteProps>(); const { teamID } = useParams<TeamsRouteProps>();
const history = useHistory(); const history = useHistory();
const { loading, data } = useGetTeamQuery({ variables: { teamID } }); const { loading, data } = useGetTeamQuery({
variables: { teamID },
onCompleted: resp => {
document.title = `${resp.findTeam.name} | Taskcafé`;
},
});
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const [currentTab, setCurrentTab] = useState(0); const [currentTab, setCurrentTab] = useState(0);
const match = useRouteMatch(); const match = useRouteMatch();
useEffect(() => {
document.title = 'Teams | Taskcafé';
}, []);
if (loading) { if (loading) {
return ( return (
<> <GlobalTopNavbar
<span>loading</span> menuType={[
</> { name: 'Projects', link: `${match.url}` },
{ name: 'Members', link: `${match.url}/members` },
]}
currentTab={currentTab}
onSetTab={tab => {
setCurrentTab(tab);
}}
onSaveProjectName={NOOP}
projectID={null}
name={null}
/>
); );
} }
if (data && user) { if (data && user) {

View File

@ -557,6 +557,7 @@ const Admin: React.FC<AdminProps> = ({
<TabNavContent> <TabNavContent>
{items.map((item, idx) => ( {items.map((item, idx) => (
<NavItem <NavItem
key={item.name}
onClick={(tab, top) => { onClick={(tab, top) => {
if ($tabNav && $tabNav.current) { if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect(); const pos = $tabNav.current.getBoundingClientRect();

View File

@ -1,9 +1,7 @@
import styled, { css, keyframes } from 'styled-components'; import styled, { css, keyframes } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { CheckCircle, CheckSquareOutline } from 'shared/icons'; import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
import { RefObject } from 'react';
import TaskAssignee from 'shared/components/TaskAssignee'; import TaskAssignee from 'shared/components/TaskAssignee';
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>` export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
@ -20,7 +18,9 @@ export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'no
stroke: rgba(${props.theme.colors.success}); stroke: rgba(${props.theme.colors.success});
`} `}
`; `;
export const ClockIcon = styled(FontAwesomeIcon)``; export const ClockIcon = styled(Clock)<{ color: string }>`
fill: ${props => props.color};
`;
export const EditorTextarea = styled(TextareaAutosize)` export const EditorTextarea = styled(TextareaAutosize)`
overflow: hidden; overflow: hidden;
@ -147,6 +147,11 @@ export const ListCardLabelText = styled.span`
line-height: 16px; line-height: 16px;
`; `;
export const ListCardLabelsWrapper = styled.div`
overflow: auto;
position: relative;
`;
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>` export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
${props => ${props =>
props.variant === 'small' props.variant === 'small'
@ -178,8 +183,6 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
`; `;
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>` export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
overflow: auto;
position: relative;
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }

View File

@ -1,7 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Pencil, Eye, List } from 'shared/icons';
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
import { faClock, faEye } from '@fortawesome/free-regular-svg-icons';
import { import {
EditorTextarea, EditorTextarea,
CardMember, CardMember,
@ -20,6 +18,7 @@ import {
ListCardLabels, ListCardLabels,
ListCardLabel, ListCardLabel,
ListCardLabelText, ListCardLabelText,
ListCardLabelsWrapper,
ListCardOperation, ListCardOperation,
CardTitle, CardTitle,
CardMembers, CardMembers,
@ -154,39 +153,42 @@ const Card = React.forwardRef(
} }
}} }}
> >
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} /> <Pencil width={8} height={8} />
</ListCardOperation> </ListCardOperation>
)} )}
<ListCardDetails complete={complete ?? false}> <ListCardDetails complete={complete ?? false}>
<ListCardLabels {labels && labels.length !== 0 && (
toggleLabels={toggleLabels} <ListCardLabelsWrapper>
toggleDirection={toggleDirection} <ListCardLabels
onClick={e => { toggleLabels={toggleLabels}
e.stopPropagation(); toggleDirection={toggleDirection}
if (onCardLabelClick) { onClick={e => {
onCardLabelClick(); e.stopPropagation();
} if (onCardLabelClick) {
}} onCardLabelClick();
> }
{labels && }}
labels >
.slice() {labels
.sort((a, b) => a.labelColor.position - b.labelColor.position) .slice()
.map(label => ( .sort((a, b) => a.labelColor.position - b.labelColor.position)
<ListCardLabel .map(label => (
onAnimationEnd={() => { <ListCardLabel
if (setToggleLabels) { onAnimationEnd={() => {
setToggleLabels(false); if (setToggleLabels) {
} setToggleLabels(false);
}} }
variant={labelVariant ?? 'large'} }}
color={label.labelColor.colorHex} variant={labelVariant ?? 'large'}
key={label.id} color={label.labelColor.colorHex}
> key={label.id}
<ListCardLabelText>{label.name}</ListCardLabelText> >
</ListCardLabel> <ListCardLabelText>{label.name}</ListCardLabelText>
))} </ListCardLabel>
</ListCardLabels> ))}
</ListCardLabels>
</ListCardLabelsWrapper>
)}
{editable ? ( {editable ? (
<EditorContent> <EditorContent>
{complete && <CompleteIcon width={16} height={16} />} {complete && <CompleteIcon width={16} height={16} />}
@ -214,18 +216,18 @@ const Card = React.forwardRef(
<ListCardBadges> <ListCardBadges>
{watched && ( {watched && (
<ListCardBadge> <ListCardBadge>
<FontAwesomeIcon color="#6b778c" icon={faEye} size="xs" /> <Eye width={8} height={8} />
</ListCardBadge> </ListCardBadge>
)} )}
{dueDate && ( {dueDate && (
<DueDateCardBadge isPastDue={dueDate.isPastDue}> <DueDateCardBadge isPastDue={dueDate.isPastDue}>
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} icon={faClock} size="xs" /> <ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} width={8} height={8} />
<ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText> <ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
</DueDateCardBadge> </DueDateCardBadge>
)} )}
{description && ( {description && (
<DescriptionBadge> <DescriptionBadge>
<FontAwesomeIcon color="#6b778c" icon={faList} size="xs" /> <List width={8} height={8} />
</DescriptionBadge> </DescriptionBadge>
)} )}
{checklists && ( {checklists && (

View File

@ -1,15 +1,15 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Button from 'shared/components/Button'; import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles'; import { mixin } from 'shared/utils/styles';
export const CancelIcon = styled(FontAwesomeIcon)` export const CancelIconWrapper = styled.div`
opacity: 0.8; opacity: 0.8;
cursor: pointer; cursor: pointer;
font-size: 1.25em; font-size: 1.25em;
padding-left: 5px; padding-left: 5px;
`; `;
export const CardComposerWrapper = styled.div<{ isOpen: boolean }>` export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
padding-bottom: 8px; padding-bottom: 8px;
display: ${props => (props.isOpen ? 'flex' : 'none')}; display: ${props => (props.isOpen ? 'flex' : 'none')};

View File

@ -1,12 +1,12 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown'; import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Cross } from 'shared/icons';
import { import {
CardComposerWrapper, CardComposerWrapper,
CancelIcon, CancelIconWrapper,
AddCardButton, AddCardButton,
ComposerControls, ComposerControls,
ComposerControlsSaveSection, ComposerControlsSaveSection,
@ -52,7 +52,9 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
> >
Add Card Add Card
</AddCardButton> </AddCardButton>
<CancelIcon onClick={onClose} icon={faTimes} color="#42526e" /> <CancelIconWrapper onClick={() => onClose()}>
<Cross width={12} height={12} />
</CancelIconWrapper>
</ComposerControlsSaveSection> </ComposerControlsSaveSection>
<ComposerControlsActionsSection /> <ComposerControlsActionsSection />
</ComposerControls> </ComposerControls>

View File

@ -78,6 +78,7 @@ const Icon = styled.div`
type InputProps = { type InputProps = {
variant?: 'normal' | 'alternate'; variant?: 'normal' | 'alternate';
disabled?: boolean;
label?: string; label?: string;
width?: string; width?: string;
floatingLabel?: boolean; floatingLabel?: boolean;
@ -116,6 +117,7 @@ function useCombinedRefs(...refs: any) {
const Input = React.forwardRef( const Input = React.forwardRef(
( (
{ {
disabled = false,
width = 'auto', width = 'auto',
variant = 'normal', variant = 'normal',
type = 'text', type = 'text',
@ -160,6 +162,7 @@ const Input = React.forwardRef(
onChange={e => { onChange={e => {
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false); setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
}} }}
disabled={disabled}
hasValue={hasValue} hasValue={hasValue}
ref={combinedRef} ref={combinedRef}
id={id} id={id}

View File

@ -27,6 +27,7 @@ export const Default = () => {
<BaseStyles /> <BaseStyles />
<Settings <Settings
profile={profile} profile={profile}
onChangeUserInfo={action('change user info')}
onResetPassword={action('reset password')} onResetPassword={action('reset password')}
onProfileAvatarRemove={action('remove')} onProfileAvatarRemove={action('remove')}
onProfileAvatarChange={action('profile avatar change')} onProfileAvatarChange={action('profile avatar change')}

View File

@ -10,6 +10,11 @@ const PasswordInput = styled(Input)`
margin-bottom: 0; margin-bottom: 0;
`; `;
const UserInfoInput = styled(Input)`
margin-top: 30px;
margin-bottom: 0;
`;
const FormError = styled.span` const FormError = styled.span`
font-size: 12px; font-size: 12px;
color: rgba(${props => props.theme.colors.warning}); color: rgba(${props => props.theme.colors.warning});
@ -240,6 +245,7 @@ const SaveButton = styled(Button)`
type SettingsProps = { type SettingsProps = {
onProfileAvatarChange: () => void; onProfileAvatarChange: () => void;
onProfileAvatarRemove: () => void; onProfileAvatarRemove: () => void;
onChangeUserInfo: (data: UserInfoData, done: () => void) => void;
onResetPassword: (password: string, done: () => void) => void; onResetPassword: (password: string, done: () => void) => void;
profile: TaskUser; profile: TaskUser;
}; };
@ -300,9 +306,93 @@ const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword })
); );
}; };
type UserInfoData = {
full_name: string;
bio: string;
initials: string;
email: string;
};
type UserInfoTabProps = {
profile: TaskUser;
onProfileAvatarChange: () => void;
onProfileAvatarRemove: () => void;
onChangeUserInfo: (data: UserInfoData, done: () => void) => void;
};
const EMAIL_PATTERN = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/i;
const INITIALS_PATTERN = /^[a-zA-Z]{2,3}$/i;
const UserInfoTab: React.FC<UserInfoTabProps> = ({
profile,
onProfileAvatarRemove,
onProfileAvatarChange,
onChangeUserInfo,
}) => {
const [active, setActive] = useState(true);
const { register, handleSubmit, errors } = useForm<UserInfoData>();
const done = () => {
setActive(true);
};
return (
<>
<AvatarSettings
onProfileAvatarRemove={onProfileAvatarRemove}
onProfileAvatarChange={onProfileAvatarChange}
profile={profile.profileIcon}
/>
<form
onSubmit={handleSubmit(data => {
setActive(false);
onChangeUserInfo(data, done);
})}
>
<UserInfoInput
ref={register({ required: 'Full name is required' })}
name="full_name"
defaultValue={profile.fullName}
width="100%"
label="Name"
/>
{errors.full_name && <FormError>{errors.full_name.message}</FormError>}
<UserInfoInput
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
ref={register({
required: 'Initials is required',
pattern: { value: INITIALS_PATTERN, message: 'Intials must be between two to four characters' },
})}
name="initials"
width="100%"
label="Initials "
/>
{errors.initials && <FormError>{errors.initials.message}</FormError>}
<UserInfoInput disabled defaultValue={profile.username ?? ''} width="100%" label="Username " />
<UserInfoInput
width="100%"
name="email"
ref={register({
required: 'Email is required',
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
})}
defaultValue={profile.email ?? ''}
label="Email"
/>
{errors.email && <FormError>{errors.email.message}</FormError>}
<UserInfoInput width="100%" name="bio" ref={register()} defaultValue={profile.bio ?? ''} label="Bio" />
{errors.bio && <FormError>{errors.bio.message}</FormError>}
<SettingActions>
<SaveButton disabled={!active} type="submit">
Save Change
</SaveButton>
</SettingActions>
</form>
</>
);
};
const Settings: React.FC<SettingsProps> = ({ const Settings: React.FC<SettingsProps> = ({
onProfileAvatarRemove, onProfileAvatarRemove,
onProfileAvatarChange, onProfileAvatarChange,
onChangeUserInfo,
onResetPassword, onResetPassword,
profile, profile,
}) => { }) => {
@ -315,6 +405,7 @@ const Settings: React.FC<SettingsProps> = ({
<TabNavContent> <TabNavContent>
{items.map((item, idx) => ( {items.map((item, idx) => (
<NavItem <NavItem
key={item.name}
onClick={(tab, top) => { onClick={(tab, top) => {
if ($tabNav && $tabNav.current) { if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect(); const pos = $tabNav.current.getBoundingClientRect();
@ -332,23 +423,12 @@ const Settings: React.FC<SettingsProps> = ({
</TabNav> </TabNav>
<TabContentWrapper> <TabContentWrapper>
<Tab tab={0} currentTab={currentTab}> <Tab tab={0} currentTab={currentTab}>
<AvatarSettings <UserInfoTab
onProfileAvatarRemove={onProfileAvatarRemove}
onProfileAvatarChange={onProfileAvatarChange} onProfileAvatarChange={onProfileAvatarChange}
profile={profile.profileIcon} onProfileAvatarRemove={onProfileAvatarRemove}
profile={profile}
onChangeUserInfo={onChangeUserInfo}
/> />
<Input defaultValue={profile.fullName} width="100%" label="Name" />
<Input
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
width="100%"
label="Initials "
/>
<Input defaultValue={profile.username ?? ''} width="100%" label="Username " />
<Input width="100%" label="Email" />
<Input width="100%" label="Bio" />
<SettingActions>
<SaveButton>Save Change</SaveButton>
</SettingActions>
</Tab> </Tab>
<Tab tab={1} currentTab={currentTab}> <Tab tab={1} currentTab={currentTab}>
<ResetPasswordTab onResetPassword={onResetPassword} /> <ResetPasswordTab onResetPassword={onResetPassword} />

View File

@ -585,3 +585,30 @@ export const ActivityItemLog = styled.span`
margin-left: 2px; margin-left: 2px;
color: rgba(${props => props.theme.colors.text.primary}); color: rgba(${props => props.theme.colors.text.primary});
`; `;
export const ViewRawButton = styled.button`
border-radius: 3px;
padding: 8px 12px;
display: flex;
position: absolute;
right: 4px;
bottom: -24px;
cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary}, 0.25);
&:hover {
color: rgba(${props => props.theme.colors.text.primary});
}
`;
export const TaskDetailsEditor = styled(TextareaAutosize)`
min-height: 108px;
color: #c2c6dc;
background: #262c49;
border-radius: 3px;
line-height: 20px;
margin-left: 32px;
margin-right: 32px;
padding: 9px 8px 7px 8px;
outline: none;
border: none;
`;

View File

@ -30,6 +30,7 @@ import {
AssignUserLabel, AssignUserLabel,
AssignUsersButton, AssignUsersButton,
AssignedUsersSection, AssignedUsersSection,
ViewRawButton,
DueDateTitle, DueDateTitle,
Container, Container,
LeftSidebar, LeftSidebar,
@ -65,6 +66,7 @@ import {
CommentProfile, CommentProfile,
CommentInnerWrapper, CommentInnerWrapper,
ActivitySection, ActivitySection,
TaskDetailsEditor,
} from './Styles'; } from './Styles';
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist'; import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
import onDragEnd from './onDragEnd'; import onDragEnd from './onDragEnd';
@ -153,6 +155,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
return true; return true;
}); });
const [saveTimeout, setSaveTimeout] = useState<any>(null); const [saveTimeout, setSaveTimeout] = useState<any>(null);
const [showRaw, setShowRaw] = useState(false);
const [showCommentActions, setShowCommentActions] = useState(false); const [showCommentActions, setShowCommentActions] = useState(false);
const taskDescriptionRef = useRef(task.description ?? ''); const taskDescriptionRef = useRef(task.description ?? '');
const $noMemberBtn = useRef<HTMLDivElement>(null); const $noMemberBtn = useRef<HTMLDivElement>(null);
@ -169,7 +172,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
<LeftSidebarSection> <LeftSidebarSection>
<SidebarTitle>TASK GROUP</SidebarTitle> <SidebarTitle>TASK GROUP</SidebarTitle>
<SidebarButton> <SidebarButton>
<SidebarButtonText>Release 0.1.0</SidebarButtonText> <SidebarButtonText>{task.taskGroup.name}</SidebarButtonText>
</SidebarButton> </SidebarButton>
<DueDateTitle>DUE DATE</DueDateTitle> <DueDateTitle>DUE DATE</DueDateTitle>
<SidebarButton <SidebarButton
@ -309,28 +312,34 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
</HeaderContainer> </HeaderContainer>
<InnerContentContainer> <InnerContentContainer>
<DescriptionContainer> <DescriptionContainer>
<EditorContainer {showRaw ? (
onClick={e => { <TaskDetailsEditor value={taskDescriptionRef.current} />
if (!editTaskDescription) { ) : (
setEditTaskDescription(true); <EditorContainer
} onClick={e => {
}} if (!editTaskDescription) {
> setEditTaskDescription(true);
<Editor }
defaultValue={task.description ?? ''}
theme={dark}
readOnly={!editTaskDescription}
autoFocus
onChange={value => {
setSaveTimeout(() => {
clearTimeout(saveTimeout);
return setTimeout(saveDescription, 2000);
});
const text = value();
taskDescriptionRef.current = text;
}} }}
/> >
</EditorContainer> <Editor
defaultValue={task.description ?? ''}
theme={dark}
readOnly={!editTaskDescription}
autoFocus
onChange={value => {
setSaveTimeout(() => {
clearTimeout(saveTimeout);
return setTimeout(saveDescription, 2000);
});
const text = value();
taskDescriptionRef.current = text;
}}
/>
</EditorContainer>
)}
<ViewRawButton onClick={() => setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'}</ViewRawButton>
</DescriptionContainer> </DescriptionContainer>
<ChecklistSection> <ChecklistSection>
<DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}> <DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>

View File

@ -251,8 +251,8 @@ export const NavSeparator = styled.div`
export const LogoContainer = styled(Link)` export const LogoContainer = styled(Link)`
display: block; display: block;
left: 50%; left: 50%;
right: 50%;
position: absolute; position: absolute;
transform: translateX(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -105,6 +105,7 @@ export type UserAccount = {
createdAt: Scalars['Time']; createdAt: Scalars['Time'];
fullName: Scalars['String']; fullName: Scalars['String'];
initials: Scalars['String']; initials: Scalars['String'];
bio: Scalars['String'];
role: Role; role: Role;
username: Scalars['String']; username: Scalars['String'];
profileIcon: ProfileIcon; profileIcon: ProfileIcon;
@ -303,6 +304,7 @@ export type Mutation = {
updateTaskLocation: UpdateTaskLocationPayload; updateTaskLocation: UpdateTaskLocationPayload;
updateTaskName: Task; updateTaskName: Task;
updateTeamMemberRole: UpdateTeamMemberRolePayload; updateTeamMemberRole: UpdateTeamMemberRolePayload;
updateUserInfo: UpdateUserInfoPayload;
updateUserPassword: UpdateUserPasswordPayload; updateUserPassword: UpdateUserPasswordPayload;
updateUserRole: UpdateUserRolePayload; updateUserRole: UpdateUserRolePayload;
}; };
@ -548,6 +550,11 @@ export type MutationUpdateTeamMemberRoleArgs = {
}; };
export type MutationUpdateUserInfoArgs = {
input: UpdateUserInfo;
};
export type MutationUpdateUserPasswordArgs = { export type MutationUpdateUserPasswordArgs = {
input: UpdateUserPassword; input: UpdateUserPassword;
}; };
@ -979,6 +986,18 @@ export type UpdateTeamMemberRolePayload = {
member: Member; member: Member;
}; };
export type UpdateUserInfoPayload = {
__typename?: 'UpdateUserInfoPayload';
user: UserAccount;
};
export type UpdateUserInfo = {
name: Scalars['String'];
initials: Scalars['String'];
email: Scalars['String'];
bio: Scalars['String'];
};
export type UpdateUserPassword = { export type UpdateUserPassword = {
userID: Scalars['UUID']; userID: Scalars['UUID'];
password: Scalars['String']; password: Scalars['String'];
@ -1245,7 +1264,7 @@ export type FindTaskQuery = (
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'> & Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'position' | 'complete'>
& { taskGroup: ( & { taskGroup: (
{ __typename?: 'TaskGroup' } { __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id'> & Pick<TaskGroup, 'id' | 'name'>
), badges: ( ), badges: (
{ __typename?: 'TaskBadges' } { __typename?: 'TaskBadges' }
& { checklist?: Maybe<( & { checklist?: Maybe<(
@ -1354,7 +1373,7 @@ export type MeQuery = (
{ __typename?: 'MePayload' } { __typename?: 'MePayload' }
& { user: ( & { user: (
{ __typename?: 'UserAccount' } { __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'fullName'> & Pick<UserAccount, 'id' | 'fullName' | 'username' | 'email' | 'bio'>
& { profileIcon: ( & { profileIcon: (
{ __typename?: 'ProfileIcon' } { __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'> & Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
@ -2080,7 +2099,7 @@ export type CreateUserAccountMutation = (
{ __typename?: 'Mutation' } { __typename?: 'Mutation' }
& { createUserAccount: ( & { createUserAccount: (
{ __typename?: 'UserAccount' } { __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username'> & Pick<UserAccount, 'id' | 'email' | 'fullName' | 'initials' | 'username' | 'bio'>
& { profileIcon: ( & { profileIcon: (
{ __typename?: 'ProfileIcon' } { __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'> & Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -2127,6 +2146,29 @@ export type DeleteUserAccountMutation = (
) } ) }
); );
export type UpdateUserInfoMutationVariables = {
name: Scalars['String'];
initials: Scalars['String'];
email: Scalars['String'];
bio: Scalars['String'];
};
export type UpdateUserInfoMutation = (
{ __typename?: 'Mutation' }
& { updateUserInfo: (
{ __typename?: 'UpdateUserInfoPayload' }
& { user: (
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'email' | 'fullName' | 'bio'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials'>
) }
) }
) }
);
export type UpdateUserPasswordMutationVariables = { export type UpdateUserPasswordMutationVariables = {
userID: Scalars['UUID']; userID: Scalars['UUID'];
password: Scalars['String']; password: Scalars['String'];
@ -2660,6 +2702,7 @@ export const FindTaskDocument = gql`
complete complete
taskGroup { taskGroup {
id id
name
} }
badges { badges {
checklist { checklist {
@ -2795,6 +2838,9 @@ export const MeDocument = gql`
user { user {
id id
fullName fullName
username
email
bio
profileIcon { profileIcon {
initials initials
bgColor bgColor
@ -4271,6 +4317,7 @@ export const CreateUserAccountDocument = gql`
fullName fullName
initials initials
username username
bio
profileIcon { profileIcon {
url url
initials initials
@ -4369,6 +4416,49 @@ export function useDeleteUserAccountMutation(baseOptions?: ApolloReactHooks.Muta
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>; export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>; export type DeleteUserAccountMutationResult = ApolloReactCommon.MutationResult<DeleteUserAccountMutation>;
export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>; export type DeleteUserAccountMutationOptions = ApolloReactCommon.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const UpdateUserInfoDocument = gql`
mutation updateUserInfo($name: String!, $initials: String!, $email: String!, $bio: String!) {
updateUserInfo(input: {name: $name, initials: $initials, email: $email, bio: $bio}) {
user {
id
email
fullName
bio
profileIcon {
initials
}
}
}
}
`;
export type UpdateUserInfoMutationFn = ApolloReactCommon.MutationFunction<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>;
/**
* __useUpdateUserInfoMutation__
*
* To run a mutation, you first call `useUpdateUserInfoMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateUserInfoMutation` 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 [updateUserInfoMutation, { data, loading, error }] = useUpdateUserInfoMutation({
* variables: {
* name: // value for 'name'
* initials: // value for 'initials'
* email: // value for 'email'
* bio: // value for 'bio'
* },
* });
*/
export function useUpdateUserInfoMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>) {
return ApolloReactHooks.useMutation<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>(UpdateUserInfoDocument, baseOptions);
}
export type UpdateUserInfoMutationHookResult = ReturnType<typeof useUpdateUserInfoMutation>;
export type UpdateUserInfoMutationResult = ApolloReactCommon.MutationResult<UpdateUserInfoMutation>;
export type UpdateUserInfoMutationOptions = ApolloReactCommon.BaseMutationOptions<UpdateUserInfoMutation, UpdateUserInfoMutationVariables>;
export const UpdateUserPasswordDocument = gql` export const UpdateUserPasswordDocument = gql`
mutation updateUserPassword($userID: UUID!, $password: String!) { mutation updateUserPassword($userID: UUID!, $password: String!) {
updateUserPassword(input: {userID: $userID, password: $password}) { updateUserPassword(input: {userID: $userID, password: $password}) {

View File

@ -8,6 +8,7 @@ query findTask($taskID: UUID!) {
complete complete
taskGroup { taskGroup {
id id
name
} }
badges { badges {
checklist { checklist {

View File

@ -3,6 +3,9 @@ query me {
user { user {
id id
fullName fullName
username
email
bio
profileIcon { profileIcon {
initials initials
bgColor bgColor

View File

@ -24,6 +24,7 @@ export const CREATE_USER_MUTATION = gql`
fullName fullName
initials initials
username username
bio
profileIcon { profileIcon {
url url
initials initials

View File

@ -0,0 +1,19 @@
import gql from 'graphql-tag';
export const UPDATE_USER_INFO_MUTATION = gql`
mutation updateUserInfo($name: String!, $initials: String!, $email: String!, $bio: String!) {
updateUserInfo(input: { name: $name, initials: $initials, email: $email, bio: $bio }) {
user {
id
email
fullName
bio
profileIcon {
initials
}
}
}
}
`;
export default UPDATE_USER_INFO_MUTATION;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Eye: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 576 512">
<path d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z" />
</Icon>
);
};
export default Eye;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const List: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
return (
<Icon width={width} height={height} className={className} viewBox="0 0 512 512">
<path d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z" />
</Icon>
);
};
export default List;

View File

@ -1,5 +1,7 @@
import Cross from './Cross'; import Cross from './Cross';
import Cog from './Cog'; import Cog from './Cog';
import Eye from './Eye';
import List from './List';
import At from './At'; import At from './At';
import Task from './Task'; import Task from './Task';
import Smile from './Smile'; import Smile from './Smile';
@ -85,4 +87,6 @@ export {
Clone, Clone,
Paperclip, Paperclip,
Share, Share,
Eye,
List,
}; };

View File

@ -46,6 +46,8 @@ type OwnedList = {
type TaskUser = { type TaskUser = {
id: string; id: string;
fullName: string; fullName: string;
email?: string;
bio?: string;
profileIcon: ProfileIcon; profileIcon: ProfileIcon;
username?: string; username?: string;
role?: Role; role?: Role;

View File

@ -157,4 +157,5 @@ type UserAccount struct {
Initials string `json:"initials"` Initials string `json:"initials"`
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"` ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
RoleCode string `json:"role_code"` RoleCode string `json:"role_code"`
Bio string `json:"bio"`
} }

View File

@ -114,6 +114,7 @@ type Querier interface {
UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error) UpdateTaskName(ctx context.Context, arg UpdateTaskNameParams) (Task, error)
UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error) UpdateTaskPosition(ctx context.Context, arg UpdateTaskPositionParams) (Task, error)
UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error) UpdateTeamMemberRole(ctx context.Context, arg UpdateTeamMemberRoleParams) (TeamMember, error)
UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccountInfoParams) (UserAccount, error)
UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg UpdateUserAccountProfileAvatarURLParams) (UserAccount, error)
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UserAccount, error)
} }

View File

@ -15,6 +15,10 @@ 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: UpdateUserAccountInfo :one
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
WHERE user_id = $1 RETURNING *;
-- name: DeleteUserAccountByID :exec -- name: DeleteUserAccountByID :exec
DELETE FROM user_account WHERE user_id = $1; DELETE FROM user_account WHERE user_id = $1;

View File

@ -13,7 +13,7 @@ import (
const createUserAccount = `-- name: CreateUserAccount :one const createUserAccount = `-- name: CreateUserAccount :one
INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code) INSERT INTO user_account(full_name, initials, email, username, created_at, password_hash, role_code)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
` `
type CreateUserAccountParams struct { type CreateUserAccountParams struct {
@ -48,6 +48,7 @@ func (q *Queries) CreateUserAccount(ctx context.Context, arg CreateUserAccountPa
&i.Initials, &i.Initials,
&i.ProfileAvatarUrl, &i.ProfileAvatarUrl,
&i.RoleCode, &i.RoleCode,
&i.Bio,
) )
return i, err return i, err
} }
@ -62,7 +63,7 @@ func (q *Queries) DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) e
} }
const getAllUserAccounts = `-- name: GetAllUserAccounts :many const getAllUserAccounts = `-- name: GetAllUserAccounts :many
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE username != 'system' SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username != 'system'
` `
func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) { func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error) {
@ -85,6 +86,7 @@ func (q *Queries) GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
&i.Initials, &i.Initials,
&i.ProfileAvatarUrl, &i.ProfileAvatarUrl,
&i.RoleCode, &i.RoleCode,
&i.Bio,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -119,7 +121,7 @@ func (q *Queries) GetRoleForUserID(ctx context.Context, userID uuid.UUID) (GetRo
} }
const getUserAccountByID = `-- name: GetUserAccountByID :one const getUserAccountByID = `-- name: GetUserAccountByID :one
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE user_id = $1 SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE user_id = $1
` `
func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) { func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (UserAccount, error) {
@ -136,12 +138,13 @@ func (q *Queries) GetUserAccountByID(ctx context.Context, userID uuid.UUID) (Use
&i.Initials, &i.Initials,
&i.ProfileAvatarUrl, &i.ProfileAvatarUrl,
&i.RoleCode, &i.RoleCode,
&i.Bio,
) )
return i, err return i, err
} }
const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one const getUserAccountByUsername = `-- name: GetUserAccountByUsername :one
SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code FROM user_account WHERE username = $1 SELECT user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio FROM user_account WHERE username = $1
` `
func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) { func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string) (UserAccount, error) {
@ -158,12 +161,13 @@ func (q *Queries) GetUserAccountByUsername(ctx context.Context, username string)
&i.Initials, &i.Initials,
&i.ProfileAvatarUrl, &i.ProfileAvatarUrl,
&i.RoleCode, &i.RoleCode,
&i.Bio,
) )
return i, err return i, err
} }
const setUserPassword = `-- name: SetUserPassword :one const setUserPassword = `-- name: SetUserPassword :one
UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code UPDATE user_account SET password_hash = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
` `
type SetUserPasswordParams struct { type SetUserPasswordParams struct {
@ -185,13 +189,52 @@ func (q *Queries) SetUserPassword(ctx context.Context, arg SetUserPasswordParams
&i.Initials, &i.Initials,
&i.ProfileAvatarUrl, &i.ProfileAvatarUrl,
&i.RoleCode, &i.RoleCode,
&i.Bio,
)
return i, err
}
const updateUserAccountInfo = `-- name: UpdateUserAccountInfo :one
UPDATE user_account SET bio = $2, full_name = $3, initials = $4, email = $5
WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
`
type UpdateUserAccountInfoParams struct {
UserID uuid.UUID `json:"user_id"`
Bio string `json:"bio"`
FullName string `json:"full_name"`
Initials string `json:"initials"`
Email string `json:"email"`
}
func (q *Queries) UpdateUserAccountInfo(ctx context.Context, arg UpdateUserAccountInfoParams) (UserAccount, error) {
row := q.db.QueryRowContext(ctx, updateUserAccountInfo,
arg.UserID,
arg.Bio,
arg.FullName,
arg.Initials,
arg.Email,
)
var i UserAccount
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.ProfileBgColor,
&i.FullName,
&i.Initials,
&i.ProfileAvatarUrl,
&i.RoleCode,
&i.Bio,
) )
return i, err return i, err
} }
const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one const updateUserAccountProfileAvatarURL = `-- name: UpdateUserAccountProfileAvatarURL :one
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 user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
` `
type UpdateUserAccountProfileAvatarURLParams struct { type UpdateUserAccountProfileAvatarURLParams struct {
@ -213,12 +256,13 @@ func (q *Queries) UpdateUserAccountProfileAvatarURL(ctx context.Context, arg Upd
&i.Initials, &i.Initials,
&i.ProfileAvatarUrl, &i.ProfileAvatarUrl,
&i.RoleCode, &i.RoleCode,
&i.Bio,
) )
return i, err return i, err
} }
const updateUserRole = `-- name: UpdateUserRole :one const updateUserRole = `-- name: UpdateUserRole :one
UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code UPDATE user_account SET role_code = $2 WHERE user_id = $1 RETURNING user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio
` `
type UpdateUserRoleParams struct { type UpdateUserRoleParams struct {
@ -240,6 +284,7 @@ func (q *Queries) UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams)
&i.Initials, &i.Initials,
&i.ProfileAvatarUrl, &i.ProfileAvatarUrl,
&i.RoleCode, &i.RoleCode,
&i.Bio,
) )
return i, err return i, err
} }

View File

@ -210,6 +210,7 @@ type ComplexityRoot struct {
UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int UpdateTaskLocation func(childComplexity int, input NewTaskLocation) int
UpdateTaskName func(childComplexity int, input UpdateTaskName) int UpdateTaskName func(childComplexity int, input UpdateTaskName) int
UpdateTeamMemberRole func(childComplexity int, input UpdateTeamMemberRole) int UpdateTeamMemberRole func(childComplexity int, input UpdateTeamMemberRole) int
UpdateUserInfo func(childComplexity int, input UpdateUserInfo) int
UpdateUserPassword func(childComplexity int, input UpdateUserPassword) int UpdateUserPassword func(childComplexity int, input UpdateUserPassword) int
UpdateUserRole func(childComplexity int, input UpdateUserRole) int UpdateUserRole func(childComplexity int, input UpdateUserRole) int
} }
@ -404,6 +405,10 @@ type ComplexityRoot struct {
TeamID func(childComplexity int) int TeamID func(childComplexity int) int
} }
UpdateUserInfoPayload struct {
User func(childComplexity int) int
}
UpdateUserPasswordPayload struct { UpdateUserPasswordPayload struct {
Ok func(childComplexity int) int Ok func(childComplexity int) int
User func(childComplexity int) int User func(childComplexity int) int
@ -414,6 +419,7 @@ type ComplexityRoot struct {
} }
UserAccount struct { UserAccount struct {
Bio func(childComplexity int) int
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Email func(childComplexity int) int Email func(childComplexity int) int
FullName func(childComplexity int) int FullName func(childComplexity int) int
@ -482,6 +488,7 @@ type MutationResolver interface {
ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error) ClearProfileAvatar(ctx context.Context) (*db.UserAccount, error)
UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error) UpdateUserPassword(ctx context.Context, input UpdateUserPassword) (*UpdateUserPasswordPayload, error)
UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error) UpdateUserRole(ctx context.Context, input UpdateUserRole) (*UpdateUserRolePayload, error)
UpdateUserInfo(ctx context.Context, input UpdateUserInfo) (*UpdateUserInfoPayload, error)
} }
type NotificationResolver interface { type NotificationResolver interface {
ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error)
@ -1493,6 +1500,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.UpdateTeamMemberRole(childComplexity, args["input"].(UpdateTeamMemberRole)), true return e.complexity.Mutation.UpdateTeamMemberRole(childComplexity, args["input"].(UpdateTeamMemberRole)), true
case "Mutation.updateUserInfo":
if e.complexity.Mutation.UpdateUserInfo == nil {
break
}
args, err := ec.field_Mutation_updateUserInfo_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateUserInfo(childComplexity, args["input"].(UpdateUserInfo)), true
case "Mutation.updateUserPassword": case "Mutation.updateUserPassword":
if e.complexity.Mutation.UpdateUserPassword == nil { if e.complexity.Mutation.UpdateUserPassword == nil {
break break
@ -2284,6 +2303,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.UpdateTeamMemberRolePayload.TeamID(childComplexity), true return e.complexity.UpdateTeamMemberRolePayload.TeamID(childComplexity), true
case "UpdateUserInfoPayload.user":
if e.complexity.UpdateUserInfoPayload.User == nil {
break
}
return e.complexity.UpdateUserInfoPayload.User(childComplexity), true
case "UpdateUserPasswordPayload.ok": case "UpdateUserPasswordPayload.ok":
if e.complexity.UpdateUserPasswordPayload.Ok == nil { if e.complexity.UpdateUserPasswordPayload.Ok == nil {
break break
@ -2305,6 +2331,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.UpdateUserRolePayload.User(childComplexity), true return e.complexity.UpdateUserRolePayload.User(childComplexity), true
case "UserAccount.bio":
if e.complexity.UserAccount.Bio == nil {
break
}
return e.complexity.UserAccount.Bio(childComplexity), true
case "UserAccount.createdAt": case "UserAccount.createdAt":
if e.complexity.UserAccount.CreatedAt == nil { if e.complexity.UserAccount.CreatedAt == nil {
break break
@ -2519,6 +2552,7 @@ type UserAccount {
createdAt: Time! createdAt: Time!
fullName: String! fullName: String!
initials: String! initials: String!
bio: String!
role: Role! role: Role!
username: String! username: String!
profileIcon: ProfileIcon! profileIcon: ProfileIcon!
@ -3163,6 +3197,19 @@ extend type Mutation {
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload! updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): updateUserRole(input: UpdateUserRole!):
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG) UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
updateUserInfo(input: UpdateUserInfo!):
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
type UpdateUserInfoPayload {
user: UserAccount!
}
input UpdateUserInfo {
name: String!
initials: String!
email: String!
bio: String!
} }
input UpdateUserPassword { input UpdateUserPassword {
@ -3921,6 +3968,20 @@ func (ec *executionContext) field_Mutation_updateTeamMemberRole_args(ctx context
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_updateUserInfo_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 UpdateUserInfo
if tmp, ok := rawArgs["input"]; ok {
arg0, err = ec.unmarshalNUpdateUserInfo2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfo(ctx, tmp)
if err != nil {
return nil, err
}
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_updateUserPassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_updateUserPassword_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{}{}
@ -9221,6 +9282,79 @@ func (ec *executionContext) _Mutation_updateUserRole(ctx context.Context, field
return ec.marshalNUpdateUserRolePayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserRolePayload(ctx, field.Selections, res) return ec.marshalNUpdateUserRolePayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserRolePayload(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_updateUserInfo(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: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_updateUserInfo_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) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().UpdateUserInfo(rctx, args["input"].(UpdateUserInfo))
}
directive1 := func(ctx context.Context) (interface{}, error) {
roles, err := ec.unmarshalNRoleLevel2ᚕgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐRoleLevelᚄ(ctx, []interface{}{"ADMIN"})
if err != nil {
return nil, err
}
level, err := ec.unmarshalNActionLevel2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐActionLevel(ctx, "ORG")
if err != nil {
return nil, err
}
typeArg, err := ec.unmarshalNObjectType2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐObjectType(ctx, "ORG")
if err != nil {
return nil, err
}
if ec.directives.HasRole == nil {
return nil, errors.New("directive hasRole is not implemented")
}
return ec.directives.HasRole(ctx, nil, directive0, roles, level, typeArg)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, err
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*UpdateUserInfoPayload); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/jordanknott/taskcafe/internal/graph.UpdateUserInfoPayload`, tmp)
})
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.(*UpdateUserInfoPayload)
fc.Result = res
return ec.marshalNUpdateUserInfoPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfoPayload(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_id(ctx context.Context, field graphql.CollectedField, obj *db.Notification) (ret graphql.Marshaler) { func (ec *executionContext) _Notification_id(ctx context.Context, field graphql.CollectedField, obj *db.Notification) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -12905,6 +13039,40 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload_member(ctx context.Cont
return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res) return ec.marshalNMember2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐMember(ctx, field.Selections, res)
} }
func (ec *executionContext) _UpdateUserInfoPayload_user(ctx context.Context, field graphql.CollectedField, obj *UpdateUserInfoPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "UpdateUserInfoPayload",
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.User, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*db.UserAccount)
fc.Result = res
return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
}
func (ec *executionContext) _UpdateUserPasswordPayload_ok(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) { func (ec *executionContext) _UpdateUserPasswordPayload_ok(ctx context.Context, field graphql.CollectedField, obj *UpdateUserPasswordPayload) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -13177,6 +13345,40 @@ func (ec *executionContext) _UserAccount_initials(ctx context.Context, field gra
return ec.marshalNString2string(ctx, field.Selections, res) return ec.marshalNString2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _UserAccount_bio(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "UserAccount",
Field: field,
Args: nil,
IsMethod: 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.Bio, 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) _UserAccount_role(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) { func (ec *executionContext) _UserAccount_role(ctx context.Context, field graphql.CollectedField, obj *db.UserAccount) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -15710,6 +15912,42 @@ func (ec *executionContext) unmarshalInputUpdateTeamMemberRole(ctx context.Conte
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputUpdateUserInfo(ctx context.Context, obj interface{}) (UpdateUserInfo, error) {
var it UpdateUserInfo
var asMap = obj.(map[string]interface{})
for k, v := range asMap {
switch k {
case "name":
var err error
it.Name, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
case "initials":
var err error
it.Initials, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
case "email":
var err error
it.Email, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
case "bio":
var err error
it.Bio, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateUserPassword(ctx context.Context, obj interface{}) (UpdateUserPassword, error) { func (ec *executionContext) unmarshalInputUpdateUserPassword(ctx context.Context, obj interface{}) (UpdateUserPassword, error) {
var it UpdateUserPassword var it UpdateUserPassword
var asMap = obj.(map[string]interface{}) var asMap = obj.(map[string]interface{})
@ -16671,6 +16909,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "updateUserInfo":
out.Values[i] = ec._Mutation_updateUserInfo(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -18235,6 +18478,33 @@ func (ec *executionContext) _UpdateTeamMemberRolePayload(ctx context.Context, se
return out return out
} }
var updateUserInfoPayloadImplementors = []string{"UpdateUserInfoPayload"}
func (ec *executionContext) _UpdateUserInfoPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserInfoPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, updateUserInfoPayloadImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("UpdateUserInfoPayload")
case "user":
out.Values[i] = ec._UpdateUserInfoPayload_user(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 updateUserPasswordPayloadImplementors = []string{"UpdateUserPasswordPayload"} var updateUserPasswordPayloadImplementors = []string{"UpdateUserPasswordPayload"}
func (ec *executionContext) _UpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserPasswordPayload) graphql.Marshaler { func (ec *executionContext) _UpdateUserPasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *UpdateUserPasswordPayload) graphql.Marshaler {
@ -18339,6 +18609,11 @@ func (ec *executionContext) _UserAccount(ctx context.Context, sel ast.SelectionS
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1) atomic.AddUint32(&invalids, 1)
} }
case "bio":
out.Values[i] = ec._UserAccount_bio(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "role": case "role":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
@ -20203,6 +20478,24 @@ func (ec *executionContext) marshalNUpdateTeamMemberRolePayload2ᚖgithubᚗcom
return ec._UpdateTeamMemberRolePayload(ctx, sel, v) return ec._UpdateTeamMemberRolePayload(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNUpdateUserInfo2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfo(ctx context.Context, v interface{}) (UpdateUserInfo, error) {
return ec.unmarshalInputUpdateUserInfo(ctx, v)
}
func (ec *executionContext) marshalNUpdateUserInfoPayload2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfoPayload(ctx context.Context, sel ast.SelectionSet, v UpdateUserInfoPayload) graphql.Marshaler {
return ec._UpdateUserInfoPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNUpdateUserInfoPayload2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserInfoPayload(ctx context.Context, sel ast.SelectionSet, v *UpdateUserInfoPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._UpdateUserInfoPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserPassword(ctx context.Context, v interface{}) (UpdateUserPassword, error) { func (ec *executionContext) unmarshalNUpdateUserPassword2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐUpdateUserPassword(ctx context.Context, v interface{}) (UpdateUserPassword, error) {
return ec.unmarshalInputUpdateUserPassword(ctx, v) return ec.unmarshalInputUpdateUserPassword(ctx, v)
} }

View File

@ -455,6 +455,17 @@ type UpdateTeamMemberRolePayload struct {
Member *Member `json:"member"` Member *Member `json:"member"`
} }
type UpdateUserInfo struct {
Name string `json:"name"`
Initials string `json:"initials"`
Email string `json:"email"`
Bio string `json:"bio"`
}
type UpdateUserInfoPayload struct {
User *db.UserAccount `json:"user"`
}
type UpdateUserPassword struct { type UpdateUserPassword struct {
UserID uuid.UUID `json:"userID"` UserID uuid.UUID `json:"userID"`
Password string `json:"password"` Password string `json:"password"`

View File

@ -78,6 +78,7 @@ type UserAccount {
createdAt: Time! createdAt: Time!
fullName: String! fullName: String!
initials: String! initials: String!
bio: String!
role: Role! role: Role!
username: String! username: String!
profileIcon: ProfileIcon! profileIcon: ProfileIcon!
@ -722,6 +723,19 @@ extend type Mutation {
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload! updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): updateUserRole(input: UpdateUserRole!):
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG) UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
updateUserInfo(input: UpdateUserInfo!):
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
type UpdateUserInfoPayload {
user: UserAccount!
}
input UpdateUserInfo {
name: String!
initials: String!
email: String!
bio: String!
} }
input UpdateUserPassword { input UpdateUserPassword {

View File

@ -826,6 +826,17 @@ func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserR
return &UpdateUserRolePayload{User: &user}, nil return &UpdateUserRolePayload{User: &user}, nil
} }
func (r *mutationResolver) UpdateUserInfo(ctx context.Context, input UpdateUserInfo) (*UpdateUserInfoPayload, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &UpdateUserInfoPayload{}, errors.New("invalid user ID")
}
user, err := r.Repository.UpdateUserAccountInfo(ctx, db.UpdateUserAccountInfoParams{
Bio: input.Bio, FullName: input.Name, Initials: input.Initials, Email: input.Email, UserID: userID,
})
return &UpdateUserInfoPayload{User: &user}, err
}
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) { func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
return obj.NotificationID, nil return obj.NotificationID, nil
} }

View File

@ -78,6 +78,7 @@ type UserAccount {
createdAt: Time! createdAt: Time!
fullName: String! fullName: String!
initials: String! initials: String!
bio: String!
role: Role! role: Role!
username: String! username: String!
profileIcon: ProfileIcon! profileIcon: ProfileIcon!

View File

@ -11,6 +11,19 @@ extend type Mutation {
updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload! updateUserPassword(input: UpdateUserPassword!): UpdateUserPasswordPayload!
updateUserRole(input: UpdateUserRole!): updateUserRole(input: UpdateUserRole!):
UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG) UpdateUserRolePayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
updateUserInfo(input: UpdateUserInfo!):
UpdateUserInfoPayload! @hasRole(roles: [ADMIN], level: ORG, type: ORG)
}
type UpdateUserInfoPayload {
user: UserAccount!
}
input UpdateUserInfo {
name: String!
initials: String!
email: String!
bio: String!
} }
input UpdateUserPassword { input UpdateUserPassword {

View File

@ -14,6 +14,7 @@ import (
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/frontend" "github.com/jordanknott/taskcafe/internal/frontend"
"github.com/jordanknott/taskcafe/internal/utils"
) )
// Frontend serves the index.html file // Frontend serves the index.html file
@ -30,7 +31,7 @@ func (h *TaskcafeHandler) Frontend(w http.ResponseWriter, r *http.Request) {
// ProfileImageUpload handles a user uploading a new avatar profile image // ProfileImageUpload handles a user uploading a new avatar profile image
func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) { func (h *TaskcafeHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
log.Info("preparing to upload file") log.Info("preparing to upload file")
userID, ok := r.Context().Value("userID").(uuid.UUID) userID, ok := r.Context().Value(utils.UserIDKey).(uuid.UUID)
if !ok { if !ok {
log.Error("not a valid uuid") log.Error("not a valid uuid")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)

View File

@ -0,0 +1 @@
ALTER TABLE user_account ADD COLUMN bio text NOT NULL DEFAULT '';