feat: add task sorting & filtering
adds filtering by task status (completion date, incomplete, completion) adds filtering by task metadata (task name, labels, members, due date) adds sorting by task name, labels, members, and due date
This commit is contained in:
committed by
Jordan Knott
parent
47782d6d86
commit
66583bb4fb
@ -430,6 +430,7 @@ const TabNavItem = styled.li`
|
||||
display: block;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const TabNavItemButton = styled.button<{ active: boolean }>`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@ -450,6 +451,10 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
|
||||
fill: rgba(115, 103, 240);
|
||||
}
|
||||
`;
|
||||
const TabItemUser = styled(User)<{ active: boolean }>`
|
||||
fill: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')}
|
||||
stroke: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')}
|
||||
`;
|
||||
|
||||
const TabNavItemSpan = styled.span`
|
||||
text-align: left;
|
||||
@ -512,7 +517,7 @@ const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<TabItemUser width={14} height={14} active={active} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
|
71
frontend/src/shared/components/Chip/index.tsx
Normal file
71
frontend/src/shared/components/Chip/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Cross } from 'shared/icons';
|
||||
|
||||
const LabelText = styled.span`
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
const Container = styled.div<{ color?: string }>`
|
||||
margin: 0.75rem;
|
||||
min-height: 26px;
|
||||
min-width: 26px;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
${props =>
|
||||
props.color
|
||||
? css`
|
||||
background: ${props.color};
|
||||
& ${LabelText} {
|
||||
color: rgba(${props.theme.colors.text.secondary});
|
||||
}
|
||||
`
|
||||
: css`
|
||||
background: rgba(${props.theme.colors.bg.primary});
|
||||
`}
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0 4px;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
`;
|
||||
|
||||
type ChipProps = {
|
||||
label: string;
|
||||
onClose?: () => void;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
const Chip: React.FC<ChipProps> = ({ label, onClose, color }) => {
|
||||
return (
|
||||
<Container color={color}>
|
||||
<LabelText>{label}</LabelText>
|
||||
{onClose && (
|
||||
<CloseButton onClick={() => onClose()}>
|
||||
<Cross width={12} height={12} />
|
||||
</CloseButton>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chip;
|
@ -35,7 +35,7 @@ export const Default = () => {
|
||||
<Wrapper>
|
||||
<Input label="Label placeholder" />
|
||||
<Input width="100%" placeholder="Placeholder" />
|
||||
<Input icon={<User size={20} />} width="100%" placeholder="Placeholder" />
|
||||
<Input icon={<User width={20} height={20} />} width="100%" placeholder="Placeholder" />
|
||||
</Wrapper>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
|
@ -18,7 +18,7 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onClos
|
||||
<Container ref={$containerRef} left={left} top={top}>
|
||||
<Wrapper>
|
||||
<ActionItem onClick={onAdminConsole}>
|
||||
<User size={16} color="#c2c6dc" />
|
||||
<User width={16} height={16} />
|
||||
<ActionTitle>Profile</ActionTitle>
|
||||
</ActionItem>
|
||||
<Separator />
|
||||
@ -54,7 +54,7 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ showAdminConsole, onAdminCons
|
||||
</>
|
||||
)}
|
||||
<ActionItem onClick={onProfile}>
|
||||
<User size={16} color="#c2c6dc" />
|
||||
<User width={16} height={16} />
|
||||
<ActionTitle>Profile</ActionTitle>
|
||||
</ActionItem>
|
||||
<ActionsList>
|
||||
|
@ -35,7 +35,7 @@ export const Default = () => {
|
||||
<Wrapper>
|
||||
<Input label="Label placeholder" />
|
||||
<Input width="100%" placeholder="Placeholder" />
|
||||
<Input icon={<User size={20} />} width="100%" placeholder="Placeholder" />
|
||||
<Input icon={<User width={20} height={20} />} width="100%" placeholder="Placeholder" />
|
||||
</Wrapper>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
|
@ -13,6 +13,249 @@ import {
|
||||
import moment from 'moment';
|
||||
|
||||
import { Container, BoardContainer, BoardWrapper } from './Styles';
|
||||
import shouldMetaFilter from './metaFilter';
|
||||
|
||||
export enum TaskMeta {
|
||||
NONE,
|
||||
TITLE,
|
||||
MEMBER,
|
||||
LABEL,
|
||||
DUE_DATE,
|
||||
}
|
||||
|
||||
export enum TaskMetaMatch {
|
||||
MATCH_ANY,
|
||||
MATCH_ALL,
|
||||
}
|
||||
|
||||
export enum TaskStatus {
|
||||
ALL,
|
||||
COMPLETE,
|
||||
INCOMPLETE,
|
||||
}
|
||||
|
||||
export enum TaskSince {
|
||||
ALL,
|
||||
TODAY,
|
||||
YESTERDAY,
|
||||
ONE_WEEK,
|
||||
TWO_WEEKS,
|
||||
THREE_WEEKS,
|
||||
}
|
||||
|
||||
export type TaskStatusFilter = {
|
||||
status: TaskStatus;
|
||||
since: TaskSince;
|
||||
};
|
||||
|
||||
export interface TaskMetaFilterName {
|
||||
meta: TaskMeta;
|
||||
value?: string | moment.Moment | null;
|
||||
id?: string | null;
|
||||
}
|
||||
|
||||
export type TaskNameMetaFilter = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export enum DueDateFilterType {
|
||||
TODAY,
|
||||
TOMORROW,
|
||||
THIS_WEEK,
|
||||
NEXT_WEEK,
|
||||
ONE_WEEK,
|
||||
TWO_WEEKS,
|
||||
THREE_WEEKS,
|
||||
OVERDUE,
|
||||
NO_DUE_DATE,
|
||||
}
|
||||
|
||||
export type DueDateMetaFilter = {
|
||||
type: DueDateFilterType;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type MemberMetaFilter = {
|
||||
id: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type LabelMetaFilter = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type TaskMetaFilters = {
|
||||
match: TaskMetaMatch;
|
||||
dueDate: DueDateMetaFilter | null;
|
||||
taskName: TaskNameMetaFilter | null;
|
||||
members: Array<MemberMetaFilter>;
|
||||
labels: Array<LabelMetaFilter>;
|
||||
};
|
||||
|
||||
export enum TaskSortingType {
|
||||
NONE,
|
||||
DUE_DATE,
|
||||
MEMBERS,
|
||||
LABELS,
|
||||
TASK_TITLE,
|
||||
}
|
||||
|
||||
export enum TaskSortingDirection {
|
||||
ASC,
|
||||
DESC,
|
||||
}
|
||||
|
||||
export type TaskSorting = {
|
||||
type: TaskSortingType;
|
||||
direction: TaskSortingDirection;
|
||||
};
|
||||
|
||||
function sortString(a: string, b: string) {
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortTasks(a: Task, b: Task, taskSorting: TaskSorting) {
|
||||
if (taskSorting.type === TaskSortingType.TASK_TITLE) {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (taskSorting.type === TaskSortingType.DUE_DATE) {
|
||||
if (a.dueDate && !b.dueDate) {
|
||||
return -1;
|
||||
}
|
||||
if (b.dueDate && !a.dueDate) {
|
||||
return 1;
|
||||
}
|
||||
return moment(a.dueDate).diff(moment(b.dueDate));
|
||||
}
|
||||
if (taskSorting.type === TaskSortingType.LABELS) {
|
||||
// sorts non-empty labels by name, then by empty label color name
|
||||
let aLabels = [];
|
||||
let bLabels = [];
|
||||
let aLabelsEmpty = [];
|
||||
let bLabelsEmpty = [];
|
||||
if (a.labels) {
|
||||
for (const aLabel of a.labels) {
|
||||
if (aLabel.projectLabel.name && aLabel.projectLabel.name !== '') {
|
||||
aLabels.push(aLabel.projectLabel.name);
|
||||
} else {
|
||||
aLabelsEmpty.push(aLabel.projectLabel.labelColor.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (b.labels) {
|
||||
for (const bLabel of b.labels) {
|
||||
if (bLabel.projectLabel.name && bLabel.projectLabel.name !== '') {
|
||||
bLabels.push(bLabel.projectLabel.name);
|
||||
} else {
|
||||
bLabelsEmpty.push(bLabel.projectLabel.labelColor.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
aLabels = aLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
bLabels = bLabels.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
aLabelsEmpty = aLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
bLabelsEmpty = bLabelsEmpty.sort((aLabel, bLabel) => sortString(aLabel, bLabel));
|
||||
if (aLabelsEmpty.length !== 0 || bLabelsEmpty.length !== 0) {
|
||||
if (aLabelsEmpty.length > bLabelsEmpty.length) {
|
||||
if (bLabels.length !== 0) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (aLabels.length < bLabels.length) {
|
||||
return 1;
|
||||
}
|
||||
if (aLabels.length > bLabels.length) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (taskSorting.type === TaskSortingType.MEMBERS) {
|
||||
let aMembers = [];
|
||||
let bMembers = [];
|
||||
if (a.assigned) {
|
||||
for (const aMember of a.assigned) {
|
||||
if (aMember.fullName) {
|
||||
aMembers.push(aMember.fullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (b.assigned) {
|
||||
for (const bMember of b.assigned) {
|
||||
if (bMember.fullName) {
|
||||
bMembers.push(bMember.fullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
aMembers = aMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
||||
bMembers = bMembers.sort((aMember, bMember) => sortString(aMember, bMember));
|
||||
if (aMembers.length < bMembers.length) {
|
||||
return 1;
|
||||
}
|
||||
if (aMembers.length > bMembers.length) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
|
||||
if (filter.status === TaskStatus.ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filter.status === TaskStatus.INCOMPLETE && task.complete === false) {
|
||||
return true;
|
||||
}
|
||||
if (filter.status === TaskStatus.COMPLETE && task.completedAt && task.complete === true) {
|
||||
const completedAt = moment(task.completedAt);
|
||||
const REFERENCE = moment(); // fixed just for testing, use moment();
|
||||
switch (filter.since) {
|
||||
case TaskSince.TODAY:
|
||||
const TODAY = REFERENCE.clone().startOf('day');
|
||||
return completedAt.isSame(TODAY, 'd');
|
||||
case TaskSince.YESTERDAY:
|
||||
const YESTERDAY = REFERENCE.clone()
|
||||
.subtract(1, 'days')
|
||||
.startOf('day');
|
||||
return completedAt.isSameOrAfter(YESTERDAY, 'd');
|
||||
case TaskSince.ONE_WEEK:
|
||||
const ONE_WEEK = REFERENCE.clone()
|
||||
.subtract(7, 'days')
|
||||
.startOf('day');
|
||||
return completedAt.isSameOrAfter(ONE_WEEK, 'd');
|
||||
case TaskSince.TWO_WEEKS:
|
||||
const TWO_WEEKS = REFERENCE.clone()
|
||||
.subtract(14, 'days')
|
||||
.startOf('day');
|
||||
return completedAt.isSameOrAfter(TWO_WEEKS, 'd');
|
||||
case TaskSince.THREE_WEEKS:
|
||||
const THREE_WEEKS = REFERENCE.clone()
|
||||
.subtract(21, 'days')
|
||||
.startOf('day');
|
||||
return completedAt.isSameOrAfter(THREE_WEEKS, 'd');
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface SimpleProps {
|
||||
taskGroups: Array<TaskGroup>;
|
||||
@ -28,8 +271,29 @@ interface SimpleProps {
|
||||
onCardMemberClick: OnCardMemberClick;
|
||||
onCardLabelClick: () => void;
|
||||
cardLabelVariant: CardLabelVariant;
|
||||
taskStatusFilter?: TaskStatusFilter;
|
||||
taskMetaFilters?: TaskMetaFilters;
|
||||
taskSorting?: TaskSorting;
|
||||
}
|
||||
|
||||
const initTaskStatusFilter: TaskStatusFilter = {
|
||||
status: TaskStatus.ALL,
|
||||
since: TaskSince.ALL,
|
||||
};
|
||||
|
||||
const initTaskMetaFilters: TaskMetaFilters = {
|
||||
match: TaskMetaMatch.MATCH_ANY,
|
||||
dueDate: null,
|
||||
taskName: null,
|
||||
labels: [],
|
||||
members: [],
|
||||
};
|
||||
|
||||
const initTaskSorting: TaskSorting = {
|
||||
type: TaskSortingType.NONE,
|
||||
direction: TaskSortingDirection.ASC,
|
||||
};
|
||||
|
||||
const SimpleLists: React.FC<SimpleProps> = ({
|
||||
taskGroups,
|
||||
onTaskDrop,
|
||||
@ -43,6 +307,9 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
cardLabelVariant,
|
||||
onExtraMenuOpen,
|
||||
onCardMemberClick,
|
||||
taskStatusFilter = initTaskStatusFilter,
|
||||
taskMetaFilters = initTaskMetaFilters,
|
||||
taskSorting = initTaskSorting,
|
||||
}) => {
|
||||
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
|
||||
if (typeof destination === 'undefined') return;
|
||||
@ -164,10 +431,18 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
|
||||
{taskGroup.tasks
|
||||
.slice()
|
||||
.filter(t => shouldStatusFilter(t, taskStatusFilter))
|
||||
.filter(t => shouldMetaFilter(t, taskMetaFilters))
|
||||
.sort((a: any, b: any) => a.position - b.position)
|
||||
.sort((a: any, b: any) => sortTasks(a, b, taskSorting))
|
||||
.map((task: Task, taskIndex: any) => {
|
||||
return (
|
||||
<Draggable key={task.id} draggableId={task.id} index={taskIndex}>
|
||||
<Draggable
|
||||
key={task.id}
|
||||
draggableId={task.id}
|
||||
index={taskIndex}
|
||||
isDragDisabled={taskSorting.type !== TaskSortingType.NONE}
|
||||
>
|
||||
{taskProvided => {
|
||||
return (
|
||||
<Card
|
||||
|
132
frontend/src/shared/components/Lists/metaFilter.ts
Normal file
132
frontend/src/shared/components/Lists/metaFilter.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { TaskMetaFilters, DueDateFilterType } from 'shared/components/Lists';
|
||||
import moment from 'moment';
|
||||
|
||||
enum ShouldFilter {
|
||||
NO_FILTER,
|
||||
VALID,
|
||||
NOT_VALID,
|
||||
}
|
||||
|
||||
function shouldFilter(cond: boolean) {
|
||||
return cond ? ShouldFilter.VALID : ShouldFilter.NOT_VALID;
|
||||
}
|
||||
|
||||
export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
||||
let isFiltered = ShouldFilter.NO_FILTER;
|
||||
if (filters.taskName) {
|
||||
isFiltered = shouldFilter(task.name.toLowerCase().startsWith(filters.taskName.name.toLowerCase()));
|
||||
}
|
||||
if (filters.dueDate) {
|
||||
if (isFiltered === ShouldFilter.NO_FILTER) {
|
||||
isFiltered = ShouldFilter.NOT_VALID;
|
||||
}
|
||||
if (filters.dueDate.type === DueDateFilterType.NO_DUE_DATE) {
|
||||
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
|
||||
}
|
||||
if (task.dueDate) {
|
||||
const taskDueDate = moment(task.dueDate);
|
||||
const today = moment();
|
||||
let start;
|
||||
let end;
|
||||
switch (filters.dueDate.type) {
|
||||
case DueDateFilterType.OVERDUE:
|
||||
isFiltered = shouldFilter(taskDueDate.isBefore(today));
|
||||
break;
|
||||
case DueDateFilterType.TODAY:
|
||||
isFiltered = shouldFilter(taskDueDate.isSame(today, 'day'));
|
||||
break;
|
||||
case DueDateFilterType.TOMORROW:
|
||||
isFiltered = shouldFilter(
|
||||
taskDueDate.isBefore(
|
||||
today
|
||||
.clone()
|
||||
.add(1, 'days')
|
||||
.endOf('day'),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case DueDateFilterType.THIS_WEEK:
|
||||
start = today
|
||||
.clone()
|
||||
.weekday(0)
|
||||
.startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.weekday(6)
|
||||
.endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.NEXT_WEEK:
|
||||
start = today
|
||||
.clone()
|
||||
.weekday(0)
|
||||
.add(7, 'days')
|
||||
.startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.weekday(6)
|
||||
.add(7, 'days')
|
||||
.endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.ONE_WEEK:
|
||||
start = today.clone().startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.add(7, 'days')
|
||||
.endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.TWO_WEEKS:
|
||||
start = today.clone().startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.add(14, 'days')
|
||||
.endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
case DueDateFilterType.THREE_WEEKS:
|
||||
start = today.clone().startOf('day');
|
||||
end = today
|
||||
.clone()
|
||||
.add(21, 'days')
|
||||
.endOf('day');
|
||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||
break;
|
||||
default:
|
||||
isFiltered = ShouldFilter.NOT_VALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filters.members.length !== 0) {
|
||||
if (isFiltered === ShouldFilter.NO_FILTER) {
|
||||
isFiltered = ShouldFilter.NOT_VALID;
|
||||
}
|
||||
for (const member of filters.members) {
|
||||
if (task.assigned) {
|
||||
if (task.assigned.findIndex(m => m.id === member.id) !== -1) {
|
||||
isFiltered = ShouldFilter.VALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filters.labels.length !== 0) {
|
||||
if (isFiltered === ShouldFilter.NO_FILTER) {
|
||||
isFiltered = ShouldFilter.NOT_VALID;
|
||||
}
|
||||
for (const label of filters.labels) {
|
||||
if (task.labels) {
|
||||
if (task.labels.findIndex(m => m.projectLabel.id === label.id) !== -1) {
|
||||
isFiltered = ShouldFilter.VALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isFiltered === ShouldFilter.NO_FILTER) {
|
||||
return true;
|
||||
}
|
||||
if (isFiltered === ShouldFilter.VALID) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
@ -53,7 +53,7 @@ const Login = ({ onSubmit }: LoginProps) => {
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
|
@ -55,7 +55,7 @@ const Register = ({ onSubmit }: RegisterProps) => {
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
@ -68,7 +68,7 @@ const Register = ({ onSubmit }: RegisterProps) => {
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
@ -84,7 +84,7 @@ const Register = ({ onSubmit }: RegisterProps) => {
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
@ -103,7 +103,7 @@ const Register = ({ onSubmit }: RegisterProps) => {
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
<User width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||
|
@ -218,7 +218,7 @@ const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<User width={20} height={20} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
|
@ -162,6 +162,7 @@ export type Task = {
|
||||
description?: Maybe<Scalars['String']>;
|
||||
dueDate?: Maybe<Scalars['Time']>;
|
||||
complete: Scalars['Boolean'];
|
||||
completedAt?: Maybe<Scalars['Time']>;
|
||||
assigned: Array<Member>;
|
||||
labels: Array<TaskLabel>;
|
||||
checklists: Array<TaskChecklist>;
|
||||
@ -1189,7 +1190,7 @@ export type FindTaskQuery = (
|
||||
|
||||
export type TaskFieldsFragment = (
|
||||
{ __typename?: 'Task' }
|
||||
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'complete' | 'position'>
|
||||
& Pick<Task, 'id' | 'name' | 'description' | 'dueDate' | 'complete' | 'completedAt' | 'position'>
|
||||
& { badges: (
|
||||
{ __typename?: 'TaskBadges' }
|
||||
& { checklist?: Maybe<(
|
||||
@ -2013,6 +2014,7 @@ export const TaskFieldsFragmentDoc = gql`
|
||||
description
|
||||
dueDate
|
||||
complete
|
||||
completedAt
|
||||
position
|
||||
badges {
|
||||
checklist {
|
||||
|
@ -7,6 +7,7 @@ const TASK_FRAGMENT = gql`
|
||||
description
|
||||
dueDate
|
||||
complete
|
||||
completedAt
|
||||
position
|
||||
badges {
|
||||
checklist {
|
||||
|
12
frontend/src/shared/icons/Calendar.tsx
Normal file
12
frontend/src/shared/icons/Calendar.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
const Calender: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||
return (
|
||||
<Icon width={width} height={height} className={className} viewBox="0 0 448 512">
|
||||
<path d="M400 64h-48V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H160V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zm-6 400H54c-3.3 0-6-2.7-6-6V160h352v298c0 3.3-2.7 6-6 6z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calender;
|
@ -1,21 +1,12 @@
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from './Icon';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const User = ({ size, color }: Props) => {
|
||||
const User: React.FC<IconProps> = ({ width = '16px', height = '16px', className, onClick }) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M9 11.041v-0.825c1.102-0.621 2-2.168 2-3.716 0-2.485 0-4.5-3-4.5s-3 2.015-3 4.5c0 1.548 0.898 3.095 2 3.716v0.825c-3.392 0.277-6 1.944-6 3.959h14c0-2.015-2.608-3.682-6-3.959z" />
|
||||
</svg>
|
||||
<Icon onClick={onClick} width={width} height={height} className={className} viewBox="0 0 448 512">
|
||||
<path d="M313.6 304c-28.7 0-42.5 16-89.6 16-47.1 0-60.8-16-89.6-16C60.2 304 0 364.2 0 438.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-25.6c0-74.2-60.2-134.4-134.4-134.4zM400 464H48v-25.6c0-47.6 38.8-86.4 86.4-86.4 14.6 0 38.3 16 89.6 16 51.7 0 74.9-16 89.6-16 47.6 0 86.4 38.8 86.4 86.4V464zM224 288c79.5 0 144-64.5 144-144S303.5 0 224 0 80 64.5 80 144s64.5 144 144 144zm0-240c52.9 0 96 43.1 96 96s-43.1 96-96 96-96-43.1-96-96 43.1-96 96-96z" />
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
User.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Cross from './Cross';
|
||||
import Cog from './Cog';
|
||||
import Calendar from './Calendar';
|
||||
import Sort from './Sort';
|
||||
import Filter from './Filter';
|
||||
import DoubleChevronUp from './DoubleChevronUp';
|
||||
@ -72,4 +73,5 @@ export {
|
||||
UserPlus,
|
||||
Crown,
|
||||
ToggleOn,
|
||||
Calendar,
|
||||
};
|
||||
|
Reference in New Issue
Block a user