feature: add project labels

This commit is contained in:
Jordan Knott
2020-05-27 16:18:50 -05:00
parent fba4de631f
commit cbcd8c5f82
42 changed files with 1024 additions and 143 deletions

View File

@ -7,8 +7,9 @@ import { useMeQuery } from 'shared/generated/graphql';
type GlobalTopNavbarProps = {
name: string;
projectMembers?: null | Array<TaskUser>;
};
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name }) => {
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name, projectMembers }) => {
const { loading, data } = useMeQuery();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
@ -50,6 +51,7 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name }) => {
lastName={data ? data.me.lastName : ''}
initials={!data ? '' : data.me.profileIcon.initials ?? ''}
onNotificationClick={() => console.log('beep')}
projectMembers={projectMembers}
onProfileClick={onProfileClick}
/>
{menu.isOpen && (

View File

@ -12,6 +12,7 @@ type KanbanBoardProps = {
onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void;
onCardMemberClick: OnCardMemberClick;
};
const KanbanBoard: React.FC<KanbanBoardProps> = ({
@ -22,6 +23,7 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onCardDrop,
onListDrop,
onCreateList,
onCardMemberClick,
}) => {
const match = useRouteMatch();
const history = useHistory();
@ -40,6 +42,7 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onListDrop={onListDrop}
{...listsData}
onCreateList={onCreateList}
onCardMemberClick={onCardMemberClick}
/>
</Board>
);

View File

@ -18,8 +18,10 @@ import {
useAssignTaskMutation,
DeleteTaskDocument,
FindProjectDocument,
useCreateProjectLabelMutation,
} from 'shared/generated/graphql';
import TaskAssignee from 'shared/components/TaskAssignee';
import QuickCardEditor from 'shared/components/QuickCardEditor';
import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
@ -30,6 +32,7 @@ import LabelManager from 'shared/components/PopupMenu/LabelManager';
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
import produce from 'immer';
import Details from './Details';
import MiniProfile from 'shared/components/MiniProfile';
type TaskRouteProps = {
taskID: string;
@ -52,14 +55,23 @@ const Title = styled.span`
font-size: 24px;
color: #fff;
`;
const ProjectMembers = styled.div`
display: flex;
padding-left: 4px;
padding-top: 4px;
align-items: center;
`;
type LabelManagerEditorProps = {
labels: Array<Label>;
projectID: string;
labelColors: Array<LabelColor>;
};
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initialLabels }) => {
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initialLabels, projectID, labelColors }) => {
const [labels, setLabels] = useState<Array<Label>>(initialLabels);
const [currentLabel, setCurrentLabel] = useState('');
const [createProjectLabel] = useCreateProjectLabelMutation();
const { setTab } = usePopup();
return (
<>
@ -74,26 +86,21 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initial
setTab(1);
}}
onLabelToggle={labelId => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
setCurrentLabel(labelId);
setTab(1);
}}
/>
</Popup>
<Popup onClose={() => {}} title="Edit label" tab={1}>
<LabelEditor
labelColors={labelColors}
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
draftState[idx] = { ...draftState[idx], name, labelColor: color };
}
}),
);
@ -103,9 +110,12 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({ labels: initial
</Popup>
<Popup onClose={() => {}} title="Create new label" tab={2}>
<LabelEditor
labelColors={labelColors}
label={null}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
console.log(name, color);
setLabels([...labels, { labelId: name, name, labelColor: color, active: false }]);
createProjectLabel({ variables: { projectID, labelColorID: color.id, name } });
setTab(0);
}}
/>
@ -124,7 +134,7 @@ const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top:
const initialLabelsPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
const initialTaskDetailsState = { isOpen: false, taskID: '' };
const ProjectActions = styled.div`
const ProjectBar = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
@ -132,6 +142,11 @@ const ProjectActions = styled.div`
padding: 0 12px;
`;
const ProjectActions = styled.div`
display: flex;
align-items: center;
`;
const ProjectAction = styled.div`
cursor: pointer;
display: flex;
@ -351,46 +366,67 @@ const Project = () => {
task: currentTask,
});
};
return (
<>
<GlobalTopNavbar name={data.findProject.name} />
<ProjectActions>
<ProjectAction
ref={$labelsRef}
onClick={() => {
showPopup(
$labelsRef,
<LabelManagerEditor
labels={data.findProject.labels.map(label => {
return {
labelId: label.id,
name: label.name ?? '',
color: label.colorHex,
active: false,
};
})}
/>,
);
}}
>
<Tags size={13} color="#c2c6dc" />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction>
<ToggleOn size={13} color="#c2c6dc" />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction>
<Bolt size={13} color="#c2c6dc" />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
<GlobalTopNavbar projectMembers={availableMembers} name={data.findProject.name} />
<ProjectBar>
<ProjectActions>
<ProjectAction
ref={$labelsRef}
onClick={() => {
showPopup(
$labelsRef,
<LabelManagerEditor
labelColors={data.labelColors}
labels={data.findProject.labels.map(label => {
return {
labelId: label.id,
name: label.name ?? '',
labelColor: label.labelColor,
active: false,
};
})}
projectID={projectId}
/>,
);
}}
>
<Tags size={13} color="#c2c6dc" />
<ProjectActionText>Labels</ProjectActionText>
</ProjectAction>
<ProjectAction>
<ToggleOn size={13} color="#c2c6dc" />
<ProjectActionText>Fields</ProjectActionText>
</ProjectAction>
<ProjectAction>
<Bolt size={13} color="#c2c6dc" />
<ProjectActionText>Rules</ProjectActionText>
</ProjectAction>
</ProjectActions>
</ProjectBar>
<KanbanBoard
listsData={currentListsData}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCardCreate={onCardCreate}
onCreateList={onCreateList}
onCardMemberClick={($targetRef, taskID, memberID) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={availableMembers[0].profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>
</Popup>,
);
}}
onQuickEditorOpen={onQuickEditorOpen}
onOpenListActionsPopup={($targetRef, taskGroupID) => {
showPopup(

12
web/src/citadel.d.ts vendored
View File

@ -84,7 +84,7 @@ type Team = {
type Label = {
labelId: string;
name: string;
color: string;
labelColor: LabelColor;
active: boolean;
};
@ -117,7 +117,17 @@ type ElementSize = {
height: number;
};
type LabelColor = {
id: string;
name: string;
colorHex: string;
position: number;
};
type OnCardMemberClick = ($targetRef: RefObject<HTMLElement>, taskID: string, memberID: string) => void;
type ElementBounds = {
size: ElementSize;
position: ElementPosition;
};

View File

@ -1,5 +1,6 @@
import styled, { css } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div``;
@ -27,11 +28,11 @@ export const Wrapper = styled.div<{ editorOpen: boolean }>`
${props =>
props.editorOpen &&
css`
background-color: #ebecf0;
background-color: #10163a;
border-radius: 3px;
height: auto;
min-height: 32px;
padding: 4px;
padding: 8px;
transition: background 85ms ease-in, opacity 40ms ease-in, border-color 85ms ease-in;
`}
`;
@ -53,17 +54,33 @@ export const ListNameEditorWrapper = styled.div`
display: flex;
`;
export const ListNameEditor = styled(TextareaAutosize)`
background: #fff;
background-color: ${props => mixin.lighten('#262c49', 0.05)};
border: none;
box-shadow: inset 0 0 0 2px #0079bf;
display: block;
margin: 0;
transition: margin 85ms ease-in, background 85ms ease-in;
width: 100%;
line-height: 20px;
padding: 8px 12px;
font-family: 'Droid Sans';
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
border: none;
border-radius: 3px;
box-shadow: none;
margin-bottom: 4px;
max-height: 162px;
min-height: 54px;
font-size: 14px;
outline: none;
line-height: 20px;
color: #c2c6dc;
l &:focus {
background-color: ${props => mixin.lighten('#262c49', 0.05)};
}
`;
export const ListAddControls = styled.div`
@ -74,10 +91,9 @@ export const ListAddControls = styled.div`
`;
export const AddListButton = styled.button`
background-color: #5aac44;
box-shadow: none;
border: none;
color: #fff;
color: #c2c6dc;
float: left;
margin: 0 4px 0 0;
cursor: pointer;
@ -88,6 +104,8 @@ export const AddListButton = styled.button`
text-align: center;
border-radius: 3px;
font-size: 14px;
background: rgb(115, 103, 240);
`;
export const CancelAdd = styled.div`

View File

@ -45,6 +45,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
onKeyDown={onKeyDown}
value={listName}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
placeholder="Enter a title for this list..."
/>
</ListNameEditorWrapper>
<ListAddControls>
@ -60,7 +61,7 @@ const NameEditor: React.FC<NameEditorProps> = ({ onSave, onCancel }) => {
Save
</AddListButton>
<CancelAdd onClick={() => onCancel()}>
<Cross />
<Cross color="#c2c6dc" />
</CancelAdd>
</ListAddControls>
</>

View File

@ -18,13 +18,23 @@ const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
labelColor: {
id: '1',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
active: false,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
labelColor: {
id: '2',
colorHex: LabelColors.PINK,
name: 'pink',
position: 2,
},
active: false,
},
];

View File

@ -1,6 +1,7 @@
import styled, { css } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { mixin } from 'shared/utils/styles';
import { RefObject } from 'react';
export const ClockIcon = styled(FontAwesomeIcon)``;
@ -127,7 +128,7 @@ export const CardMembers = styled.div`
margin: 0 -2px 0 0;
`;
export const CardMember = styled.div<{ bgColor: string }>`
export const CardMember = styled.div<{ bgColor: string; ref: any }>`
height: 28px;
width: 28px;
float: right;

View File

@ -33,6 +33,31 @@ type Checklist = {
total: number;
};
type MemberProps = {
onCardMemberClick?: OnCardMemberClick;
taskID: string;
member: TaskUser;
};
const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member }) => {
const $targetRef = useRef<HTMLDivElement>();
return (
<CardMember
ref={$targetRef}
onClick={e => {
if (onCardMemberClick) {
e.stopPropagation();
onCardMemberClick($targetRef, taskID, member.userID);
}
}}
key={member.userID}
bgColor={member.profileIcon.bgColor ?? '#7367F0'}
>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
);
};
type Props = {
title: string;
description: string;
@ -46,6 +71,7 @@ type Props = {
labels?: Label[];
wrapperProps?: any;
members?: Array<TaskUser> | null;
onCardMemberClick?: OnCardMemberClick;
};
const Card = React.forwardRef(
@ -63,6 +89,7 @@ const Card = React.forwardRef(
checklists,
watched,
members,
onCardMemberClick,
}: Props,
$cardRef: any,
) => {
@ -109,7 +136,7 @@ const Card = React.forwardRef(
<ListCardLabels>
{labels &&
labels.map(label => (
<ListCardLabel color={label.color} key={label.name}>
<ListCardLabel color={label.labelColor.colorHex} key={label.name}>
{label.name}
</ListCardLabel>
))}
@ -141,11 +168,7 @@ const Card = React.forwardRef(
</ListCardBadges>
<CardMembers>
{members &&
members.map(member => (
<CardMember key={member.userID} bgColor={member.profileIcon.bgColor ?? '#7367F0'}>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
))}
members.map(member => <Member taskID={taskID} member={member} onCardMemberClick={onCardMemberClick} />)}
</CardMembers>
</ListCardDetails>
</ListCardInnerContainer>

View File

@ -75,10 +75,10 @@ export const ComposerControlsActionsSection = styled.div`
`;
export const AddCardButton = styled.button`
background-color: #5aac44;
background: rgb(115, 103, 240);
box-shadow: none;
border: none;
color: #fff;
color: #c2c6dc;
cursor: pointer;
display: inline-block;
font-weight: 400;

View File

@ -21,7 +21,14 @@ export const Default = () => {
taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world',
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
labels: [
{
labelId: 'soft-skills',
labelColor: { id: '1', colorHex: '#fff', name: 'white', position: 1 },
active: true,
name: 'Soft Skills',
},
],
description: 'hello!',
members: [
{ userID: '1', profileIcon: { url: null, initials: null, bgColor: null }, displayName: 'Jordan Knott' },

View File

@ -20,13 +20,23 @@ const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
labelColor: {
id: '1',
colorHex: LabelColors.BLUE,
name: 'blue',
position: 1,
},
active: false,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
labelColor: {
id: '2',
colorHex: LabelColors.PINK,
name: 'pink',
position: 2,
},
active: false,
},
];

View File

@ -96,6 +96,7 @@ export const Default = () => {
onQuickEditorOpen={action('card composer open')}
onCardDrop={onCardDrop}
onListDrop={onListDrop}
onCardMemberClick={action('card member click')}
onCardCreate={action('card create')}
onCreateList={listName => {
const [lastColumn] = Object.values(listsData.columns)
@ -209,6 +210,7 @@ export const ListsWithManyList = () => {
onListDrop={onListDrop}
onCreateList={action('create list')}
onExtraMenuOpen={action('extra menu open')}
onCardMemberClick={action('card member click')}
/>
);
};

View File

@ -30,6 +30,7 @@ type Props = {
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
onCardMemberClick: OnCardMemberClick;
};
const Lists: React.FC<Props> = ({
@ -41,6 +42,7 @@ const Lists: React.FC<Props> = ({
onCardCreate,
onQuickEditorOpen,
onCreateList,
onCardMemberClick,
onExtraMenuOpen,
}) => {
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
@ -161,6 +163,7 @@ const Lists: React.FC<Props> = ({
labels={task.labels}
members={task.members}
onClick={() => onCardClick(task)}
onCardMemberClick={onCardMemberClick}
onContextMenu={onQuickEditorOpen}
/>
);

View File

@ -4,14 +4,15 @@ import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
type Props = {
labelColors: Array<LabelColor>;
label: Label | null;
onLabelEdit: (labelId: string | null, labelName: string, color: string) => void;
onLabelEdit: (labelId: string | null, labelName: string, labelColor: LabelColor) => void;
};
const LabelManager = ({ label, onLabelEdit }: Props) => {
const LabelManager = ({ labelColors, label, onLabelEdit }: Props) => {
console.log(label);
const [currentLabel, setCurrentLabel] = useState(label ? label.name : '');
const [currentColor, setCurrentColor] = useState<string | null>(label ? label.color : null);
const [currentColor, setCurrentColor] = useState<LabelColor | null>(label ? label.labelColor : null);
return (
<EditLabelForm>
<FieldLabel>Name</FieldLabel>
@ -26,14 +27,14 @@ const LabelManager = ({ label, onLabelEdit }: Props) => {
/>
<FieldLabel>Select a color</FieldLabel>
<div>
{Object.values(LabelColors).map(labelColor => (
{labelColors.map((labelColor: LabelColor) => (
<LabelBox
color={labelColor}
color={labelColor.colorHex}
onClick={() => {
setCurrentColor(labelColor);
}}
>
{labelColor === currentColor && <Checkmark color="#fff" size={12} />}
{currentColor && labelColor.id === currentColor.id && <Checkmark color="#fff" size={12} />}
</LabelBox>
))}
</div>

View File

@ -49,7 +49,7 @@ const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit, onL
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
color={label.labelColor.colorHex}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);

View File

@ -28,13 +28,23 @@ const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
active: true,
labelColor: {
id: '1',
name: 'white',
colorHex: LabelColors.BLUE,
position: 1,
},
active: false,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
labelColor: {
id: '1',
name: 'white',
colorHex: LabelColors.PINK,
position: 1,
},
active: false,
},
];
@ -75,13 +85,14 @@ const LabelManagerEditor = () => {
</Popup>
<Popup onClose={action('on close')} title="Edit label" tab={1}>
<LabelEditor
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
draftState[idx] = { ...draftState[idx], name, labelColor: color };
}
}),
);
@ -92,8 +103,9 @@ const LabelManagerEditor = () => {
<Popup onClose={action('on close')} title="Create new label" tab={2}>
<LabelEditor
label={null}
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
setLabels([...labels, { labelId: name, name, labelColor: color, active: false }]);
setTab(0);
}}
/>
@ -141,7 +153,11 @@ export const LabelsLabelEditor = () => {
onClose={() => setPopupOpen(false)}
left={10}
>
<LabelEditor label={labelData[0]} onLabelEdit={action('label edit')} />
<LabelEditor
label={labelData[0]}
onLabelEdit={action('label edit')}
labelColors={[{ id: '1', colorHex: '#c2c6dc', position: 1, name: 'gray' }]}
/>
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
@ -239,7 +255,19 @@ export const DueDateManagerPopup = () => {
taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world',
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
labels: [
{
labelId: 'soft-skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
active: true,
name: 'Soft Skills',
},
],
description: 'hello!',
members: [
{ userID: '1', profileIcon: { bgColor: null, url: null, initials: null }, displayName: 'Jordan Knott' },
@ -325,4 +353,3 @@ export const MiniProfilePopup = () => {
</>
);
};

View File

@ -207,22 +207,24 @@ export const FieldName = styled.input`
margin: 4px 0 12px;
width: 100%;
box-sizing: border-box;
border-radius: 3px;
display: block;
line-height: 20px;
margin-bottom: 12px;
padding: 8px 12px;
background: #262c49;
outline: none;
color: #c2c6dc;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: #414561;
font-size: 12px;
font-weight: 400;
color: #c2c6dc;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};

View File

@ -21,13 +21,23 @@ const labelData = [
{
labelId: 'development',
name: 'Development',
color: LabelColors.BLUE,
labelColor: {
id: '1',
name: 'white',
colorHex: LabelColors.BLUE,
position: 1,
},
active: false,
},
{
labelId: 'general',
name: 'General',
color: LabelColors.PINK,
labelColor: {
id: '1',
name: 'white',
colorHex: LabelColors.PINK,
position: 1,
},
active: false,
},
];

View File

@ -72,7 +72,7 @@ const QuickCardEditor = ({
<ListCardLabels>
{labels &&
labels.map(label => (
<ListCardLabel color={label.color} key={label.name}>
<ListCardLabel color={label.labelColor.colorHex} key={label.name}>
{label.name}
</ListCardLabel>
))}

View File

@ -0,0 +1,40 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
const TaskDetailAssignee = styled.div`
&:hover {
opacity: 0.8;
}
margin-right: 4px;
`;
const ProfileIcon = styled.div<{ size: string | number }>`
width: ${props => props.size}px;
height: ${props => props.size}px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 400;
background: rgb(115, 103, 240);
font-size: 14px;
cursor: pointer;
`;
type TaskAssigneeProps = {
size: number | string;
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile, size }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.userID)} key={member.userID}>
<ProfileIcon size={size}>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
};
export default TaskAssignee;

View File

@ -199,6 +199,7 @@ export const TaskDetailAssignee = styled.div`
}
margin-right: 4px;
`;
export const ProfileIcon = styled.div`
width: 32px;
height: 32px;

View File

@ -33,7 +33,19 @@ export const Default = () => {
taskGroup: { name: 'General', taskGroupID: '1' },
name: 'Hello, world',
position: 1,
labels: [{ labelId: 'soft-skills', color: '#fff', active: true, name: 'Soft Skills' }],
labels: [
{
labelId: 'soft-skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
active: true,
name: 'Soft Skills',
},
],
description,
members: [
{

View File

@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { Bin, Cross, Plus } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import TaskAssignee from 'shared/components/TaskAssignee';
import {
NoDueDateLabel,
@ -93,19 +94,6 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
);
};
type TaskAssigneeProps = {
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.userID)} key={member.userID}>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
};
type TaskDetailsProps = {
task: Task;
onTaskNameChange: (task: Task, newName: string) => void;
@ -210,7 +198,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : (
<>
{task.members &&
task.members.map(member => <TaskAssignee member={member} onMemberProfile={onMemberProfile} />)}
task.members.map(member => (
<TaskAssignee size={32} member={member} onMemberProfile={onMemberProfile} />
))}
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon>
<Plus size={16} color="#c2c6dc" />

View File

@ -5,6 +5,11 @@ export const NavbarWrapper = styled.div`
width: 100%;
`;
export const ProjectMembers = styled.div`
display: flex;
padding-right: 18px;
align-items: center;
`;
export const NavbarHeader = styled.header`
height: 80px;
padding: 0 1.75rem;
@ -174,3 +179,29 @@ export const ProjectSettingsButton = styled.button`
background: rgb(115, 103, 240);
}
`;
export const InviteButton = styled.button`
outline: none;
border: none;
width: 100%;
line-height: 20px;
padding: 6px 12px;
background-color: none;
text-align: center;
color: #c2c6dc;
font-size: 14px;
cursor: pointer;
margin: 0 0 0 8px;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: #414561;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -3,6 +3,7 @@ import { Star, Bell, Cog, AngleDown } from 'shared/icons';
import {
NotificationContainer,
InviteButton,
GlobalActions,
ProjectActions,
ProjectSwitcher,
@ -21,7 +22,11 @@ import {
ProfileNameWrapper,
ProfileNamePrimary,
ProfileNameSecondary,
ProjectMembers,
} from './Styles';
import TaskAssignee from 'shared/components/TaskAssignee';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import MiniProfile from 'shared/components/MiniProfile';
type NavBarProps = {
projectName: string;
@ -31,6 +36,7 @@ type NavBarProps = {
firstName: string;
lastName: string;
initials: string;
projectMembers?: Array<TaskUser> | null;
};
const NavBar: React.FC<NavBarProps> = ({
projectName,
@ -40,6 +46,7 @@ const NavBar: React.FC<NavBarProps> = ({
lastName,
initials,
bgColor,
projectMembers,
}) => {
const $profileRef: any = useRef(null);
const handleProfileClick = () => {
@ -47,6 +54,21 @@ const NavBar: React.FC<NavBarProps> = ({
const boundingRect = $profileRef.current.getBoundingClientRect();
onProfileClick(boundingRect.bottom, boundingRect.right);
};
const { showPopup } = usePopup();
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={projectMembers ? projectMembers[0].profileIcon : { url: null, initials: 'JK', bgColor: '#000' }}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {}}
/>
</Popup>,
);
};
return (
<NavbarWrapper>
<NavbarHeader>
@ -58,7 +80,9 @@ const NavBar: React.FC<NavBarProps> = ({
<ProjectSettingsButton>
<AngleDown color="#c2c6dc" />
</ProjectSettingsButton>
<Star filled color="#c2c6dc" />
<ProjectSettingsButton>
<Star width={16} height={16} color="#c2c6dc" />
</ProjectSettingsButton>
</ProjectMeta>
<ProjectTabs>
<ProjectTab active>Board</ProjectTab>
@ -68,6 +92,14 @@ const NavBar: React.FC<NavBarProps> = ({
</ProjectTabs>
</ProjectActions>
<GlobalActions>
{projectMembers && (
<ProjectMembers>
{projectMembers.map(member => (
<TaskAssignee size={28} member={member} onMemberProfile={onMemberProfile} />
))}
<InviteButton>Invite</InviteButton>
</ProjectMembers>
)}
<NotificationContainer onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} />
</NotificationContainer>

View File

@ -19,10 +19,18 @@ export type ProjectLabel = {
__typename?: 'ProjectLabel';
id: Scalars['ID'];
createdDate: Scalars['Time'];
colorHex: Scalars['String'];
labelColor: LabelColor;
name?: Maybe<Scalars['String']>;
};
export type LabelColor = {
__typename?: 'LabelColor';
id: Scalars['ID'];
name: Scalars['String'];
position: Scalars['Float'];
colorHex: Scalars['String'];
};
export type TaskLabel = {
__typename?: 'TaskLabel';
id: Scalars['ID'];
@ -130,6 +138,7 @@ export type Query = {
findProject: Project;
findTask: Task;
projects: Array<Project>;
labelColors: Array<LabelColor>;
taskGroups: Array<TaskGroup>;
me: UserAccount;
};
@ -401,7 +410,11 @@ export type CreateProjectLabelMutation = (
{ __typename?: 'Mutation' }
& { createProjectLabel: (
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'>
& Pick<ProjectLabel, 'id' | 'createdDate' | 'name'>
& { labelColor: (
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'colorHex'>
) }
) }
);
@ -499,7 +512,11 @@ export type FindProjectQuery = (
) }
)>, labels: Array<(
{ __typename?: 'ProjectLabel' }
& Pick<ProjectLabel, 'id' | 'createdDate' | 'colorHex' | 'name'>
& Pick<ProjectLabel, 'id' | 'createdDate' | 'name'>
& { labelColor: (
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'name' | 'colorHex' | 'position'>
) }
)>, taskGroups: Array<(
{ __typename?: 'TaskGroup' }
& Pick<TaskGroup, 'id' | 'name' | 'position'>
@ -516,7 +533,10 @@ export type FindProjectQuery = (
)> }
)> }
)> }
) }
), labelColors: Array<(
{ __typename?: 'LabelColor' }
& Pick<LabelColor, 'id' | 'position' | 'colorHex' | 'name'>
)> }
);
export type FindTaskQueryVariables = {
@ -692,7 +712,10 @@ export const CreateProjectLabelDocument = gql`
createProjectLabel(input: {projectID: $projectID, labelColorID: $labelColorID, name: $name}) {
id
createdDate
colorHex
labelColor {
id
colorHex
}
name
}
}
@ -899,7 +922,12 @@ export const FindProjectDocument = gql`
labels {
id
createdDate
colorHex
labelColor {
id
name
colorHex
position
}
name
}
taskGroups {
@ -924,6 +952,12 @@ export const FindProjectDocument = gql`
}
}
}
labelColors {
id
position
colorHex
name
}
}
`;

View File

@ -2,7 +2,10 @@ mutation createProjectLabel($projectID: UUID!, $labelColorID: UUID!, $name: Stri
createProjectLabel(input:{projectID:$projectID, labelColorID: $labelColorID, name: $name}) {
id
createdDate
colorHex
labelColor {
id
colorHex
}
name
}
}

View File

@ -14,7 +14,12 @@ query findProject($projectId: String!) {
labels {
id
createdDate
colorHex
labelColor {
id
name
colorHex
position
}
name
}
taskGroups {
@ -39,4 +44,10 @@ query findProject($projectId: String!) {
}
}
}
labelColors {
id
position
colorHex
name
}
}