feature: add project labels
This commit is contained in:
@ -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 && (
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
12
web/src/citadel.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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`
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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' },
|
||||
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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 = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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)};
|
||||
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
@ -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>
|
||||
))}
|
||||
|
40
web/src/shared/components/TaskAssignee/index.tsx
Normal file
40
web/src/shared/components/TaskAssignee/index.tsx
Normal 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;
|
@ -199,6 +199,7 @@ export const TaskDetailAssignee = styled.div`
|
||||
}
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
export const ProfileIcon = styled.div`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
@ -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: [
|
||||
{
|
||||
|
@ -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" />
|
||||
|
@ -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);
|
||||
}
|
||||
`;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user