feature: add first time install process
This commit is contained in:
@ -19,20 +19,20 @@ export const RoleCheckmark = styled(Checkmark)`
|
||||
`;
|
||||
|
||||
const permissions = [
|
||||
{
|
||||
code: 'owner',
|
||||
name: 'Owner',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
|
||||
},
|
||||
{
|
||||
code: 'admin',
|
||||
name: 'Admin',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
|
||||
},
|
||||
{
|
||||
code: 'owner',
|
||||
name: 'Owner',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
|
||||
},
|
||||
{
|
||||
code: 'admin',
|
||||
name: 'Admin',
|
||||
description:
|
||||
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
|
||||
},
|
||||
|
||||
{ code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.' },
|
||||
{ code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.' },
|
||||
];
|
||||
|
||||
export const RoleName = styled.div`
|
||||
@ -59,13 +59,13 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
|
||||
text-decoration: none;
|
||||
|
||||
${props =>
|
||||
props.disabled
|
||||
? css`
|
||||
props.disabled
|
||||
? css`
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
color: rgba(${props.theme.colors.text.primary}, 0.4);
|
||||
`
|
||||
: css`
|
||||
: css`
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgb(115, 103, 240);
|
||||
@ -104,178 +104,198 @@ export const RemoveMemberButton = styled(Button)`
|
||||
width: 100%;
|
||||
`;
|
||||
type TeamRoleManagerPopupProps = {
|
||||
user: TaskUser;
|
||||
warning?: string | null;
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
updateUserPassword?: (user: TaskUser, password: string) => void;
|
||||
onRemoveFromTeam?: () => void;
|
||||
user: TaskUser;
|
||||
warning?: string | null;
|
||||
canChangeRole: boolean;
|
||||
onChangeRole: (roleCode: RoleCode) => void;
|
||||
updateUserPassword?: (user: TaskUser, password: string) => void;
|
||||
onRemoveFromTeam?: () => void;
|
||||
};
|
||||
|
||||
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
||||
warning,
|
||||
user,
|
||||
canChangeRole,
|
||||
onRemoveFromTeam,
|
||||
updateUserPassword,
|
||||
onChangeRole,
|
||||
warning,
|
||||
user,
|
||||
canChangeRole,
|
||||
onRemoveFromTeam,
|
||||
updateUserPassword,
|
||||
onChangeRole,
|
||||
}) => {
|
||||
const { hidePopup, setTab } = usePopup();
|
||||
const [userPass, setUserPass] = useState({ pass: "", passConfirm: "" });
|
||||
return (
|
||||
<>
|
||||
<Popup title={null} tab={0}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{user.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(1);
|
||||
}}
|
||||
>
|
||||
Change permissions...
|
||||
const { hidePopup, setTab } = usePopup();
|
||||
const [userPass, setUserPass] = useState({ pass: '', passConfirm: '' });
|
||||
return (
|
||||
<>
|
||||
<Popup title={null} tab={0}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{user.role && (
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(1);
|
||||
}}
|
||||
>
|
||||
Change permissions...
|
||||
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
<MiniProfileActionItem onClick={() => {
|
||||
setTab(3)
|
||||
}}>Reset password...</MiniProfileActionItem>
|
||||
<MiniProfileActionItem onClick={() => setTab(5)}>Remove from organzation...</MiniProfileActionItem>
|
||||
</MiniProfileActionWrapper>
|
||||
</MiniProfileActions>
|
||||
{warning && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>{warning}</WarningText>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{permissions
|
||||
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||
.map(perm => (
|
||||
<MiniProfileActionItem
|
||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||
key={perm.code}
|
||||
onClick={() => {
|
||||
if (onChangeRole && user.role && perm.code !== user.role.code) {
|
||||
switch (perm.code) {
|
||||
case 'owner':
|
||||
onChangeRole(RoleCode.Owner);
|
||||
break;
|
||||
case 'admin':
|
||||
onChangeRole(RoleCode.Admin);
|
||||
break;
|
||||
case 'member':
|
||||
onChangeRole(RoleCode.Member);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
hidePopup();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RoleName>
|
||||
{perm.name}
|
||||
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
|
||||
</RoleName>
|
||||
<RoleDescription>{perm.description}</RoleDescription>
|
||||
</MiniProfileActionItem>
|
||||
))}
|
||||
</MiniProfileActionWrapper>
|
||||
{user.role && user.role.code === 'owner' && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>You can't change roles because there must be an owner.</WarningText>
|
||||
</>
|
||||
)}
|
||||
</MiniProfileActions>
|
||||
</Popup>
|
||||
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
The member will be removed from all cards on this project. They will receive a notification.
|
||||
</MiniProfileActionItem>
|
||||
)}
|
||||
<MiniProfileActionItem
|
||||
onClick={() => {
|
||||
setTab(3);
|
||||
}}
|
||||
>
|
||||
Reset password...
|
||||
</MiniProfileActionItem>
|
||||
<MiniProfileActionItem onClick={() => setTab(5)}>Remove from organzation...</MiniProfileActionItem>
|
||||
</MiniProfileActionWrapper>
|
||||
</MiniProfileActions>
|
||||
{warning && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>{warning}</WarningText>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
|
||||
<MiniProfileActions>
|
||||
<MiniProfileActionWrapper>
|
||||
{permissions
|
||||
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||
.map(perm => (
|
||||
<MiniProfileActionItem
|
||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||
key={perm.code}
|
||||
onClick={() => {
|
||||
if (onChangeRole && user.role && perm.code !== user.role.code) {
|
||||
switch (perm.code) {
|
||||
case 'owner':
|
||||
onChangeRole(RoleCode.Owner);
|
||||
break;
|
||||
case 'admin':
|
||||
onChangeRole(RoleCode.Admin);
|
||||
break;
|
||||
case 'member':
|
||||
onChangeRole(RoleCode.Member);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
hidePopup();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RoleName>
|
||||
{perm.name}
|
||||
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
|
||||
</RoleName>
|
||||
<RoleDescription>{perm.description}</RoleDescription>
|
||||
</MiniProfileActionItem>
|
||||
))}
|
||||
</MiniProfileActionWrapper>
|
||||
{user.role && user.role.code === 'owner' && (
|
||||
<>
|
||||
<Separator />
|
||||
<WarningText>You can't change roles because there must be an owner.</WarningText>
|
||||
</>
|
||||
)}
|
||||
</MiniProfileActions>
|
||||
</Popup>
|
||||
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
The member will be removed from all cards on this project. They will receive a notification.
|
||||
</DeleteDescription>
|
||||
<RemoveMemberButton
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
if (onRemoveFromTeam) {
|
||||
onRemoveFromTeam();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove Member
|
||||
<RemoveMemberButton
|
||||
color="danger"
|
||||
onClick={() => {
|
||||
if (onRemoveFromTeam) {
|
||||
onRemoveFromTeam();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove Member
|
||||
</RemoveMemberButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
You can either set the user's new password directly or send the user an email allowing them to reset their own password.
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password?" onClose={() => hidePopup()} tab={3}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
You can either set the user's new password directly or send the user an email allowing them to reset their
|
||||
own password.
|
||||
</DeleteDescription>
|
||||
<UserPassBar>
|
||||
<UserPassButton onClick={() => setTab(4)} color="warning">Set password...</UserPassButton>
|
||||
<UserPassButton color="warning" variant="outline">Send reset link</UserPassButton>
|
||||
</UserPassBar>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password" onClose={() => hidePopup()} tab={4}>
|
||||
<Content>
|
||||
<NewUserPassInput onChange={e => setUserPass({ pass: e.currentTarget.value, passConfirm: userPass.passConfirm })} value={userPass.pass} width="100%" variant="alternate" placeholder="New password" />
|
||||
<NewUserPassInput onChange={e => setUserPass({ passConfirm: e.currentTarget.value, pass: userPass.pass })} value={userPass.passConfirm} width="100%" variant="alternate" placeholder="New password (confirm)" />
|
||||
<UserPassConfirmButton disabled={userPass.pass === "" || userPass.passConfirm === ""} onClick={() => {
|
||||
if (userPass.pass === userPass.passConfirm && updateUserPassword) {
|
||||
updateUserPassword(user, userPass.pass)
|
||||
}
|
||||
}} color="danger">Set password</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Remove user" onClose={() => hidePopup()} tab={5}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||
<UserPassBar>
|
||||
<UserPassButton onClick={() => setTab(4)} color="warning">
|
||||
Set password...
|
||||
</UserPassButton>
|
||||
<UserPassButton color="warning" variant="outline">
|
||||
Send reset link
|
||||
</UserPassButton>
|
||||
</UserPassBar>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Reset password" onClose={() => hidePopup()} tab={4}>
|
||||
<Content>
|
||||
<NewUserPassInput defaultValue={userPass.pass} width="100%" variant="alternate" placeholder="New password" />
|
||||
<NewUserPassInput
|
||||
defaultValue={userPass.passConfirm}
|
||||
width="100%"
|
||||
variant="alternate"
|
||||
placeholder="New password (confirm)"
|
||||
/>
|
||||
<UserPassConfirmButton
|
||||
disabled={userPass.pass === '' || userPass.passConfirm === ''}
|
||||
onClick={() => {
|
||||
if (userPass.pass === userPass.passConfirm && updateUserPassword) {
|
||||
updateUserPassword(user, userPass.pass);
|
||||
}
|
||||
}}
|
||||
color="danger"
|
||||
>
|
||||
Set password
|
||||
</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
<Popup title="Remove user" onClose={() => hidePopup()} tab={5}>
|
||||
<Content>
|
||||
<DeleteDescription>
|
||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||
</DeleteDescription>
|
||||
<DeleteDescription>
|
||||
The user is the owner of 3 projects & 2 teams.
|
||||
</DeleteDescription>
|
||||
<UserSelect onChange={() => { }} value={null} options={[{ label: 'Jordan Knott', value: "jordanknott" }]} />
|
||||
<UserPassConfirmButton onClick={() => { }} color="danger">Set password</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
<DeleteDescription>The user is the owner of 3 projects & 2 teams.</DeleteDescription>
|
||||
<UserSelect onChange={() => {}} value={null} options={[{ label: 'Jordan Knott', value: 'jordanknott' }]} />
|
||||
<UserPassConfirmButton onClick={() => {}} color="danger">
|
||||
Set password
|
||||
</UserPassConfirmButton>
|
||||
</Content>
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const UserSelect = styled(Select)`
|
||||
margin: 8px 0;
|
||||
padding: 8px 0;
|
||||
`
|
||||
margin: 8px 0;
|
||||
padding: 8px 0;
|
||||
`;
|
||||
|
||||
const NewUserPassInput = styled(Input)`
|
||||
margin: 8px 0;
|
||||
`
|
||||
margin: 8px 0;
|
||||
`;
|
||||
const InviteMemberButton = styled(Button)`
|
||||
padding: 7px 12px;
|
||||
`;
|
||||
|
||||
const UserPassBar = styled.div`
|
||||
display: flex;
|
||||
padding-top: 8px;
|
||||
`
|
||||
display: flex;
|
||||
padding-top: 8px;
|
||||
`;
|
||||
const UserPassConfirmButton = styled(Button)`
|
||||
width: 100%;
|
||||
padding: 7px 12px;
|
||||
`
|
||||
`;
|
||||
|
||||
const UserPassButton = styled(Button)`
|
||||
width: 50%;
|
||||
padding: 7px 12px;
|
||||
& ~ & {
|
||||
margin-left: 6px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const MemberItemOptions = styled.div``;
|
||||
|
||||
@ -413,7 +433,7 @@ const LockUserIcon = styled(Lock)``;
|
||||
const DeleteUserIcon = styled(Trash)``;
|
||||
|
||||
type ActionButtonProps = {
|
||||
onClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
const ActionButtonWrapper = styled.div`
|
||||
@ -423,84 +443,84 @@ const ActionButtonWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => {
|
||||
const $wrapper = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
|
||||
{children}
|
||||
</ActionButtonWrapper>
|
||||
);
|
||||
const $wrapper = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
|
||||
{children}
|
||||
</ActionButtonWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionButtons = (params: any) => {
|
||||
return (
|
||||
<>
|
||||
<ActionButton onClick={() => { }}>
|
||||
<EditUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
|
||||
<DeleteUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ActionButton onClick={() => {}}>
|
||||
<EditUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
|
||||
<DeleteUserIcon width={16} height={16} />
|
||||
</ActionButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type ListTableProps = {
|
||||
users: Array<User>;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
users: Array<User>;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
};
|
||||
|
||||
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
|
||||
const data = {
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
const data = {
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
minWidth: 55,
|
||||
width: 55,
|
||||
headerCheckboxSelection: true,
|
||||
checkboxSelection: true,
|
||||
},
|
||||
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
|
||||
{ minWidth: 225, headerName: 'Email', field: 'email' },
|
||||
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
|
||||
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
|
||||
{
|
||||
minWidth: 200,
|
||||
headerName: 'Actions',
|
||||
field: 'id',
|
||||
cellRenderer: 'actionButtons',
|
||||
cellRendererParams: {
|
||||
onDeleteUser: (target: any, userID: any) => {
|
||||
onDeleteUser(target, userID);
|
||||
},
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
minWidth: 55,
|
||||
width: 55,
|
||||
headerCheckboxSelection: true,
|
||||
checkboxSelection: true,
|
||||
},
|
||||
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
|
||||
{ minWidth: 225, headerName: 'Email', field: 'email' },
|
||||
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
|
||||
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
|
||||
{
|
||||
minWidth: 200,
|
||||
headerName: 'Actions',
|
||||
field: 'id',
|
||||
cellRenderer: 'actionButtons',
|
||||
cellRendererParams: {
|
||||
onDeleteUser: (target: any, userID: any) => {
|
||||
onDeleteUser(target, userID);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
frameworkComponents: {
|
||||
actionButtons: ActionButtons,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Root>
|
||||
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
|
||||
<AgGridReact
|
||||
rowSelection="multiple"
|
||||
defaultColDef={data.defaultColDef}
|
||||
columnDefs={data.columnDefs}
|
||||
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
|
||||
frameworkComponents={data.frameworkComponents}
|
||||
onFirstDataRendered={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
onGridSizeChanged={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
},
|
||||
],
|
||||
frameworkComponents: {
|
||||
actionButtons: ActionButtons,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Root>
|
||||
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
|
||||
<AgGridReact
|
||||
rowSelection="multiple"
|
||||
defaultColDef={data.defaultColDef}
|
||||
columnDefs={data.columnDefs}
|
||||
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
|
||||
frameworkComponents={data.frameworkComponents}
|
||||
onFirstDataRendered={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
onGridSizeChanged={params => {
|
||||
params.api.sizeColumnsToFit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@ -604,138 +624,145 @@ const TabContent = styled.div`
|
||||
const items = [{ name: 'Members' }, { name: 'Settings' }];
|
||||
|
||||
type NavItemProps = {
|
||||
active: boolean;
|
||||
name: string;
|
||||
tab: number;
|
||||
onClick: (tab: number, top: number) => void;
|
||||
active: boolean;
|
||||
name: string;
|
||||
tab: number;
|
||||
onClick: (tab: number, top: number) => void;
|
||||
};
|
||||
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
|
||||
const $item = useRef<HTMLLIElement>(null);
|
||||
return (
|
||||
<TabNavItem
|
||||
key={name}
|
||||
ref={$item}
|
||||
onClick={() => {
|
||||
if ($item && $item.current) {
|
||||
const pos = $item.current.getBoundingClientRect();
|
||||
onClick(tab, pos.top);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
);
|
||||
const $item = useRef<HTMLLIElement>(null);
|
||||
return (
|
||||
<TabNavItem
|
||||
key={name}
|
||||
ref={$item}
|
||||
onClick={() => {
|
||||
if ($item && $item.current) {
|
||||
const pos = $item.current.getBoundingClientRect();
|
||||
onClick(tab, pos.top);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabNavItemButton active={active}>
|
||||
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
|
||||
<TabNavItemSpan>{name}</TabNavItemSpan>
|
||||
</TabNavItemButton>
|
||||
</TabNavItem>
|
||||
);
|
||||
};
|
||||
|
||||
type AdminProps = {
|
||||
initialTab: number;
|
||||
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
users: Array<User>;
|
||||
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
||||
initialTab: number;
|
||||
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
|
||||
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
|
||||
users: Array<User>;
|
||||
onUpdateUserPassword: (user: TaskUser, password: string) => void;
|
||||
};
|
||||
|
||||
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onUpdateUserPassword, onDeleteUser, onInviteUser, users }) => {
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
const [currentTop, setTop] = useState(initialTab * 48);
|
||||
const [currentTab, setTab] = useState(initialTab);
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const $tabNav = useRef<HTMLDivElement>(null);
|
||||
const Admin: React.FC<AdminProps> = ({
|
||||
initialTab,
|
||||
onAddUser,
|
||||
onUpdateUserPassword,
|
||||
onDeleteUser,
|
||||
onInviteUser,
|
||||
users,
|
||||
}) => {
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
const [currentTop, setTop] = useState(initialTab * 48);
|
||||
const [currentTab, setTab] = useState(initialTab);
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const $tabNav = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [updateUserRole] = useUpdateUserRoleMutation()
|
||||
return (
|
||||
<Container>
|
||||
<TabNav ref={$tabNav}>
|
||||
<TabNavContent>
|
||||
{items.map((item, idx) => (
|
||||
<NavItem
|
||||
onClick={(tab, top) => {
|
||||
if ($tabNav && $tabNav.current) {
|
||||
const pos = $tabNav.current.getBoundingClientRect();
|
||||
setTab(tab);
|
||||
setTop(top - pos.top);
|
||||
}
|
||||
}}
|
||||
name={item.name}
|
||||
tab={idx}
|
||||
active={idx === currentTab}
|
||||
/>
|
||||
))}
|
||||
<TabNavLine top={currentTop} />
|
||||
</TabNavContent>
|
||||
</TabNav>
|
||||
<TabContentWrapper>
|
||||
<TabContent>
|
||||
<MemberListWrapper>
|
||||
<MemberListHeader>
|
||||
<ListTitle>{`Users (${users.length})`}</ListTitle>
|
||||
<ListDesc>
|
||||
Team members can view and join all Team Visible boards and create new boards in the team.
|
||||
const [updateUserRole] = useUpdateUserRoleMutation();
|
||||
return (
|
||||
<Container>
|
||||
<TabNav ref={$tabNav}>
|
||||
<TabNavContent>
|
||||
{items.map((item, idx) => (
|
||||
<NavItem
|
||||
onClick={(tab, top) => {
|
||||
if ($tabNav && $tabNav.current) {
|
||||
const pos = $tabNav.current.getBoundingClientRect();
|
||||
setTab(tab);
|
||||
setTop(top - pos.top);
|
||||
}
|
||||
}}
|
||||
name={item.name}
|
||||
tab={idx}
|
||||
active={idx === currentTab}
|
||||
/>
|
||||
))}
|
||||
<TabNavLine top={currentTop} />
|
||||
</TabNavContent>
|
||||
</TabNav>
|
||||
<TabContentWrapper>
|
||||
<TabContent>
|
||||
<MemberListWrapper>
|
||||
<MemberListHeader>
|
||||
<ListTitle>{`Users (${users.length})`}</ListTitle>
|
||||
<ListDesc>
|
||||
Team members can view and join all Team Visible boards and create new boards in the team.
|
||||
</ListDesc>
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
onAddUser($target);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
New Member
|
||||
<ListActions>
|
||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||
<InviteMemberButton
|
||||
onClick={$target => {
|
||||
onAddUser($target);
|
||||
}}
|
||||
>
|
||||
<InviteIcon width={16} height={16} />
|
||||
New Member
|
||||
</InviteMemberButton>
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
{users.map(member => (
|
||||
<MemberListItem>
|
||||
<MemberProfile showRoleIcons size={32} onMemberProfile={() => { }} member={member} />
|
||||
<MemberListItemDetails>
|
||||
<MemberItemName>{member.fullName}</MemberItemName>
|
||||
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
|
||||
</MemberListItemDetails>
|
||||
<MemberItemOptions>
|
||||
<MemberItemOption variant="flat">On 6 projects</MemberItemOption>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
user={member}
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
updateUserPassword={(user, password) => {
|
||||
onUpdateUserPassword(user, password)
|
||||
}}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {
|
||||
updateUserRole({ variables: { userID: member.id, roleCode } })
|
||||
}}
|
||||
onRemoveFromTeam={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
hidePopup();
|
||||
}
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</ListActions>
|
||||
</MemberListHeader>
|
||||
<MemberList>
|
||||
{users.map(member => (
|
||||
<MemberListItem>
|
||||
<MemberProfile showRoleIcons size={32} onMemberProfile={() => {}} member={member} />
|
||||
<MemberListItemDetails>
|
||||
<MemberItemName>{member.fullName}</MemberItemName>
|
||||
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
|
||||
</MemberListItemDetails>
|
||||
<MemberItemOptions>
|
||||
<MemberItemOption variant="flat">On 6 projects</MemberItemOption>
|
||||
<MemberItemOption
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<TeamRoleManagerPopup
|
||||
user={member}
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
updateUserPassword={(user, password) => {
|
||||
onUpdateUserPassword(user, password);
|
||||
}}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {
|
||||
updateUserRole({ variables: { userID: member.id, roleCode } });
|
||||
}}
|
||||
onRemoveFromTeam={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
hidePopup();
|
||||
}
|
||||
}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</MemberItemOption>
|
||||
</MemberItemOptions>
|
||||
</MemberListItem>
|
||||
))}
|
||||
</MemberList>
|
||||
</MemberListWrapper>
|
||||
</TabContent>
|
||||
</TabContentWrapper>
|
||||
</Container>
|
||||
);
|
||||
</MemberItemOptions>
|
||||
</MemberListItem>
|
||||
))}
|
||||
</MemberList>
|
||||
</MemberListWrapper>
|
||||
</TabContent>
|
||||
</TabContentWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Admin;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import styled, {css} from 'styled-components';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {mixin} from 'shared/utils/styles';
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import {CheckCircle} from 'shared/icons';
|
||||
import {RefObject} from 'react';
|
||||
import { CheckCircle } from 'shared/icons';
|
||||
import { RefObject } from 'react';
|
||||
|
||||
export const ClockIcon = styled(FontAwesomeIcon)``;
|
||||
|
||||
@ -57,7 +57,7 @@ export const DescriptionBadge = styled(ListCardBadge)`
|
||||
padding-right: 6px;
|
||||
`;
|
||||
|
||||
export const DueDateCardBadge = styled(ListCardBadge) <{isPastDue: boolean}>`
|
||||
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
|
||||
font-size: 12px;
|
||||
${props =>
|
||||
props.isPastDue &&
|
||||
@ -76,7 +76,7 @@ export const ListCardBadgeText = styled.span`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const ListCardContainer = styled.div<{isActive: boolean; editable: boolean}>`
|
||||
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
|
||||
max-width: 256px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 3px;
|
||||
@ -93,7 +93,7 @@ export const ListCardInnerContainer = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const ListCardDetails = styled.div<{complete: boolean}>`
|
||||
export const ListCardDetails = styled.div<{ complete: boolean }>`
|
||||
overflow: hidden;
|
||||
padding: 6px 8px 2px;
|
||||
position: relative;
|
||||
@ -102,28 +102,93 @@ export const ListCardDetails = styled.div<{complete: boolean}>`
|
||||
${props => props.complete && 'opacity: 0.6;'}
|
||||
`;
|
||||
|
||||
export const ListCardLabels = styled.div`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
const labelVariantExpandAnimation = keyframes`
|
||||
0% {min-width: 40px; height: 8px;}
|
||||
50% {min-width: 56px; height: 8px;}
|
||||
100% {min-width: 56px; height: 16px;}
|
||||
`;
|
||||
|
||||
export const ListCardLabel = styled.span`
|
||||
height: 16px;
|
||||
const labelTextVariantExpandAnimation = keyframes`
|
||||
0% {transform: scale(0); visibility: hidden; pointer-events: none;}
|
||||
75% {transform: scale(0); visibility: hidden; pointer-events: none;}
|
||||
100% {transform: scale(1); visibility: visible; pointer-events: all;}
|
||||
`;
|
||||
|
||||
const labelVariantShrinkAnimation = keyframes`
|
||||
0% {min-width: 56px; height: 16px;}
|
||||
50% {min-width: 56px; height: 8px;}
|
||||
100% {min-width: 40px; height: 8px;}
|
||||
`;
|
||||
|
||||
const labelTextVariantShrinkAnimation = keyframes`
|
||||
0% {transform: scale(1); visibility: visible; pointer-events: all;}
|
||||
75% {transform: scale(0); visibility: hidden; pointer-events: none;}
|
||||
100% {transform: scale(0); visibility: hidden; pointer-events: none;}
|
||||
`;
|
||||
export const ListCardLabelText = styled.span`
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
`;
|
||||
|
||||
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
||||
${props =>
|
||||
props.variant === 'small'
|
||||
? css`
|
||||
height: 8px;
|
||||
min-width: 40px;
|
||||
& ${ListCardLabelText} {
|
||||
transform: scale(0);
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
`
|
||||
: css`
|
||||
height: 16px;
|
||||
min-width: 56px;
|
||||
`}
|
||||
|
||||
padding: 0 8px;
|
||||
max-width: 198px;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin: 0 4px 4px 0;
|
||||
width: auto;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
display: block;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background-color: ${props => props.color};
|
||||
`;
|
||||
|
||||
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
${props =>
|
||||
props.toggleLabels &&
|
||||
props.toggleDirection === 'expand' &&
|
||||
css`
|
||||
& ${ListCardLabel} {
|
||||
animation: ${labelVariantExpandAnimation} 0.45s ease-out;
|
||||
}
|
||||
& ${ListCardLabelText} {
|
||||
animation: ${labelTextVariantExpandAnimation} 0.45s ease-out;
|
||||
}
|
||||
`}
|
||||
${props =>
|
||||
props.toggleLabels &&
|
||||
props.toggleDirection === 'shrink' &&
|
||||
css`
|
||||
& ${ListCardLabel} {
|
||||
animation: ${labelVariantShrinkAnimation} 0.45s ease-out;
|
||||
}
|
||||
& ${ListCardLabelText} {
|
||||
animation: ${labelTextVariantShrinkAnimation} 0.45s ease-out;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
export const ListCardOperation = styled.span`
|
||||
display: flex;
|
||||
align-content: center;
|
||||
@ -136,7 +201,7 @@ export const ListCardOperation = styled.span`
|
||||
top: 2px;
|
||||
z-index: 100;
|
||||
&:hover {
|
||||
background-color: ${props => mixin.darken('#262c49', .25)};
|
||||
background-color: ${props => mixin.darken('#262c49', 0.25)};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, {useState, useRef, useEffect} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import {faPencilAlt, faList} from '@fortawesome/free-solid-svg-icons';
|
||||
import {faClock, faCheckSquare, faEye} from '@fortawesome/free-regular-svg-icons';
|
||||
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faClock, faCheckSquare, faEye } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
EditorTextarea,
|
||||
EditorContent,
|
||||
@ -18,6 +18,7 @@ import {
|
||||
ClockIcon,
|
||||
ListCardLabels,
|
||||
ListCardLabel,
|
||||
ListCardLabelText,
|
||||
ListCardOperation,
|
||||
CardTitle,
|
||||
CardMembers,
|
||||
@ -47,10 +48,12 @@ type Props = {
|
||||
watched?: boolean;
|
||||
wrapperProps?: any;
|
||||
members?: Array<TaskUser> | null;
|
||||
onCardLabelClick?: () => void;
|
||||
onCardMemberClick?: OnCardMemberClick;
|
||||
editable?: boolean;
|
||||
onEditCard?: (taskGroupID: string, taskID: string, cardName: string) => void;
|
||||
onCardTitleChange?: (name: string) => void;
|
||||
labelVariant?: CardLabelVariant;
|
||||
};
|
||||
|
||||
const Card = React.forwardRef(
|
||||
@ -69,14 +72,20 @@ const Card = React.forwardRef(
|
||||
checklists,
|
||||
watched,
|
||||
members,
|
||||
labelVariant,
|
||||
onCardMemberClick,
|
||||
editable,
|
||||
onCardLabelClick,
|
||||
onEditCard,
|
||||
onCardTitleChange,
|
||||
}: Props,
|
||||
$cardRef: any,
|
||||
) => {
|
||||
const [currentCardTitle, setCardTitle] = useState(title);
|
||||
const [toggleLabels, setToggleLabels] = useState(false);
|
||||
const [toggleDirection, setToggleDirection] = useState<'shrink' | 'expand'>(
|
||||
labelVariant === 'large' ? 'shrink' : 'expand',
|
||||
);
|
||||
const $editorRef: any = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
@ -132,21 +141,39 @@ const Card = React.forwardRef(
|
||||
>
|
||||
<ListCardInnerContainer ref={$innerCardRef}>
|
||||
{isActive && (
|
||||
<ListCardOperation onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (onContextMenu) {
|
||||
onContextMenu($innerCardRef, taskID, taskGroupID);
|
||||
}
|
||||
}}>
|
||||
<ListCardOperation
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (onContextMenu) {
|
||||
onContextMenu($innerCardRef, taskID, taskGroupID);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
|
||||
</ListCardOperation>
|
||||
)}
|
||||
<ListCardDetails complete={complete ?? false}>
|
||||
<ListCardLabels>
|
||||
<ListCardLabels
|
||||
toggleLabels={toggleLabels}
|
||||
toggleDirection={toggleDirection}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (onCardLabelClick) {
|
||||
setToggleLabels(true);
|
||||
setToggleDirection(labelVariant === 'large' ? 'shrink' : 'expand');
|
||||
onCardLabelClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{labels &&
|
||||
labels.map(label => (
|
||||
<ListCardLabel color={label.labelColor.colorHex} key={label.id}>
|
||||
{label.name}
|
||||
<ListCardLabel
|
||||
onAnimationEnd={() => setToggleLabels(false)}
|
||||
variant={labelVariant ?? 'large'}
|
||||
color={label.labelColor.colorHex}
|
||||
key={label.id}
|
||||
>
|
||||
<ListCardLabelText>{label.name}</ListCardLabelText>
|
||||
</ListCardLabel>
|
||||
))}
|
||||
</ListCardLabels>
|
||||
@ -169,11 +196,11 @@ const Card = React.forwardRef(
|
||||
/>
|
||||
</EditorContent>
|
||||
) : (
|
||||
<CardTitle>
|
||||
{complete && <CompleteIcon width={16} height={16} />}
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<CardTitle>
|
||||
{complete && <CompleteIcon width={16} height={16} />}
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<ListCardBadges>
|
||||
{watched && (
|
||||
<ListCardBadge>
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { getYear, getMonth } from 'date-fns';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
|
||||
type DueDateManagerProps = {
|
||||
task: Task;
|
||||
@ -147,7 +147,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
const { register, handleSubmit, errors, setValue, setError, formState } = useForm<DueDateFormData>();
|
||||
const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>();
|
||||
const saveDueDate = (data: any) => {
|
||||
console.log(data);
|
||||
const newDate = moment(`${data.endDate} ${data.endTime}`, 'YYYY-MM-DD h:mm A');
|
||||
@ -155,27 +155,16 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
onDueDateChange(task, newDate.toDate());
|
||||
}
|
||||
};
|
||||
console.log(errors);
|
||||
register({ name: 'endTime' }, { required: 'End time is required' });
|
||||
useEffect(() => {
|
||||
setValue('endTime', now.format('h:mm A'));
|
||||
}, []);
|
||||
const CustomTimeInput = forwardRef(({ value, onClick }: any, $ref: any) => {
|
||||
return (
|
||||
<DueDateInput
|
||||
id="endTime"
|
||||
name="endTime"
|
||||
ref={$ref}
|
||||
onChange={e => {
|
||||
console.log(`onCahnge ${e.currentTarget.value}`);
|
||||
setTextEndTime(e.currentTarget.value);
|
||||
setValue('endTime', e.currentTarget.value);
|
||||
}}
|
||||
width="100%"
|
||||
variant="alternate"
|
||||
label="Date"
|
||||
onClick={onClick}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -190,30 +179,29 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
||||
width="100%"
|
||||
variant="alternate"
|
||||
label="Date"
|
||||
onChange={e => {
|
||||
setTextStartDate(e.currentTarget.value);
|
||||
}}
|
||||
value={textStartDate}
|
||||
defaultValue={textStartDate}
|
||||
ref={register({
|
||||
required: 'End date is required.',
|
||||
})}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<DatePicker
|
||||
selected={endTime}
|
||||
onChange={date => {
|
||||
const changedDate = moment(date ?? new Date());
|
||||
console.log(`changed ${date}`);
|
||||
setEndTime(changedDate.toDate());
|
||||
setValue('endTime', changedDate.format('h:mm A'));
|
||||
}}
|
||||
showTimeSelect
|
||||
showTimeSelectOnly
|
||||
timeIntervals={15}
|
||||
timeCaption="Time"
|
||||
dateFormat="h:mm aa"
|
||||
customInput={<CustomTimeInput />}
|
||||
<Controller
|
||||
control={control}
|
||||
name="endTime"
|
||||
render={({ onChange, onBlur, value }) => (
|
||||
<DatePicker
|
||||
onChange={onChange}
|
||||
selected={value}
|
||||
onBlur={onBlur}
|
||||
showTimeSelect
|
||||
showTimeSelectOnly
|
||||
timeIntervals={15}
|
||||
timeCaption="Time"
|
||||
dateFormat="h:mm aa"
|
||||
customInput={<CustomTimeInput />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<DueDatePickerWrapper>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import styled, {css} from 'styled-components/macro';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
|
||||
const InputWrapper = styled.div<{width: string}>`
|
||||
const InputWrapper = styled.div<{ width: string }>`
|
||||
position: relative;
|
||||
width: ${props => props.width};
|
||||
display: flex;
|
||||
@ -14,7 +14,7 @@ const InputWrapper = styled.div<{width: string}>`
|
||||
margin-top: 24px;
|
||||
`;
|
||||
|
||||
const InputLabel = styled.span<{width: string}>`
|
||||
const InputLabel = styled.span<{ width: string }>`
|
||||
width: ${props => props.width};
|
||||
padding: 0.7rem !important;
|
||||
color: #c2c6dc;
|
||||
@ -87,9 +87,8 @@ type InputProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
className?: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
const Input = React.forwardRef(
|
||||
@ -102,40 +101,34 @@ const Input = React.forwardRef(
|
||||
placeholder,
|
||||
icon,
|
||||
name,
|
||||
onChange,
|
||||
className,
|
||||
onClick,
|
||||
floatingLabel,
|
||||
value: initialValue,
|
||||
defaultValue,
|
||||
id,
|
||||
}: InputProps,
|
||||
$ref: any,
|
||||
) => {
|
||||
const [value, setValue] = useState(initialValue ?? '');
|
||||
useEffect(() => {
|
||||
if (initialValue) {
|
||||
setValue(initialValue);
|
||||
}
|
||||
}, [initialValue]);
|
||||
const [hasValue, setHasValue] = useState(false);
|
||||
const borderColor = variant === 'normal' ? 'rgba(0, 0, 0, 0.2)' : '#414561';
|
||||
const focusBg = variant === 'normal' ? 'rgba(38, 44, 73, )' : 'rgba(16, 22, 58, 1)';
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.currentTarget.value);
|
||||
if (onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<InputWrapper className={className} width={width}>
|
||||
<InputInput
|
||||
hasValue={floatingLabel || value !== ''}
|
||||
onChange={() => {
|
||||
console.log(`change ${$ref}!`);
|
||||
if ($ref && $ref.current) {
|
||||
console.log(`value : ${$ref.current.value}`);
|
||||
setHasValue(($ref.current.value !== '' || floatingLabel) ?? false);
|
||||
}
|
||||
}}
|
||||
hasValue={hasValue}
|
||||
ref={$ref}
|
||||
id={id}
|
||||
name={name}
|
||||
onClick={onClick}
|
||||
onChange={handleChange}
|
||||
autoComplete={autocomplete ? 'on' : 'off'}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
hasIcon={typeof icon !== 'undefined'}
|
||||
width={width}
|
||||
placeholder={placeholder}
|
||||
|
@ -93,6 +93,8 @@ export const Default = () => {
|
||||
onTaskDrop={onCardDrop}
|
||||
onTaskGroupDrop={onListDrop}
|
||||
onChangeTaskGroupName={action('change group name')}
|
||||
cardLabelVariant="large"
|
||||
onCardLabelClick={action('label click')}
|
||||
onCreateTaskGroup={action('create list')}
|
||||
onExtraMenuOpen={action('extra menu open')}
|
||||
onCardMemberClick={action('card member click')}
|
||||
|
@ -26,17 +26,21 @@ interface SimpleProps {
|
||||
onCreateTaskGroup: (listName: string) => void;
|
||||
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||
onCardMemberClick: OnCardMemberClick;
|
||||
onCardLabelClick: () => void;
|
||||
cardLabelVariant: CardLabelVariant;
|
||||
}
|
||||
|
||||
const SimpleLists: React.FC<SimpleProps> = ({
|
||||
taskGroups,
|
||||
onTaskDrop,
|
||||
onChangeTaskGroupName,
|
||||
onCardLabelClick,
|
||||
onTaskGroupDrop,
|
||||
onTaskClick,
|
||||
onCreateTask,
|
||||
onQuickEditorOpen,
|
||||
onCreateTaskGroup,
|
||||
cardLabelVariant,
|
||||
onExtraMenuOpen,
|
||||
onCardMemberClick,
|
||||
}) => {
|
||||
@ -158,10 +162,12 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
{taskProvided => {
|
||||
return (
|
||||
<Card
|
||||
labelVariant={cardLabelVariant}
|
||||
wrapperProps={{
|
||||
...taskProvided.draggableProps,
|
||||
...taskProvided.dragHandleProps,
|
||||
}}
|
||||
onCardLabelClick={onCardLabelClick}
|
||||
ref={taskProvided.innerRef}
|
||||
taskID={task.id}
|
||||
complete={task.complete ?? false}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Checkmark } from 'shared/icons';
|
||||
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
|
||||
import styled from 'styled-components';
|
||||
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
|
||||
|
||||
const WhiteCheckmark = styled(Checkmark)`
|
||||
fill: rgba(${props => props.theme.colors.text.secondary});
|
||||
@ -39,17 +39,19 @@ const LabelManager = ({ labelColors, label, onLabelEdit, onLabelDelete }: Props)
|
||||
/>
|
||||
<FieldLabel>Select a color</FieldLabel>
|
||||
<div>
|
||||
{labelColors.map((labelColor: LabelColor) => (
|
||||
<LabelBox
|
||||
key={labelColor.id}
|
||||
color={labelColor.colorHex}
|
||||
onClick={() => {
|
||||
setCurrentColor(labelColor);
|
||||
}}
|
||||
>
|
||||
{currentColor && labelColor.id === currentColor.id && <WhiteCheckmark width={12} height={12} />}
|
||||
</LabelBox>
|
||||
))}
|
||||
{labelColors
|
||||
.filter(l => l.name !== 'no_color')
|
||||
.map((labelColor: LabelColor) => (
|
||||
<LabelBox
|
||||
key={labelColor.id}
|
||||
color={labelColor.colorHex}
|
||||
onClick={() => {
|
||||
setCurrentColor(labelColor);
|
||||
}}
|
||||
>
|
||||
{currentColor && labelColor.id === currentColor.id && <WhiteCheckmark width={12} height={12} />}
|
||||
</LabelBox>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<SaveButton
|
||||
|
@ -40,7 +40,7 @@ export const ListSeparator = styled.hr`
|
||||
type Props = {
|
||||
onDeleteProject: () => void;
|
||||
};
|
||||
const ProjectSettings: React.FC<Props> = ({ onDeleteProject }) => {
|
||||
const ProjectSettings: React.FC<Props> = ({onDeleteProject}) => {
|
||||
return (
|
||||
<>
|
||||
<ListActionsWrapper>
|
||||
@ -55,7 +55,7 @@ const ProjectSettings: React.FC<Props> = ({ onDeleteProject }) => {
|
||||
type TeamSettingsProps = {
|
||||
onDeleteTeam: () => void;
|
||||
};
|
||||
export const TeamSettings: React.FC<TeamSettingsProps> = ({ onDeleteTeam }) => {
|
||||
export const TeamSettings: React.FC<TeamSettingsProps> = ({onDeleteTeam}) => {
|
||||
return (
|
||||
<>
|
||||
<ListActionsWrapper>
|
||||
@ -74,6 +74,7 @@ const ConfirmSubTitle = styled.h3`
|
||||
`;
|
||||
|
||||
const ConfirmDescription = styled.div`
|
||||
margin: 0 12px;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
@ -83,7 +84,7 @@ const DeleteList = styled.ul`
|
||||
const DeleteListItem = styled.li`
|
||||
padding: 6px 0;
|
||||
list-style: disc;
|
||||
margin-left: 12px;
|
||||
margin-left: 16px;
|
||||
`;
|
||||
|
||||
const ConfirmDeleteButton = styled(Button)`
|
||||
@ -108,7 +109,7 @@ export const DELETE_INFO = {
|
||||
},
|
||||
};
|
||||
|
||||
const DeleteConfirm: React.FC<DeleteConfirmProps> = ({ description, deletedItems, onConfirmDelete }) => {
|
||||
const DeleteConfirm: React.FC<DeleteConfirmProps> = ({description, deletedItems, onConfirmDelete}) => {
|
||||
return (
|
||||
<ConfirmWrapper>
|
||||
<ConfirmDescription>
|
||||
@ -126,5 +127,5 @@ const DeleteConfirm: React.FC<DeleteConfirmProps> = ({ description, deletedItems
|
||||
);
|
||||
};
|
||||
|
||||
export { DeleteConfirm };
|
||||
export {DeleteConfirm};
|
||||
export default ProjectSettings;
|
||||
|
103
frontend/src/shared/components/Register/Styles.ts
Normal file
103
frontend/src/shared/components/Register/Styles.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import styled from 'styled-components';
|
||||
import Button from 'shared/components/Button';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
background: #eff2f7;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const Column = styled.div`
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const LoginFormWrapper = styled.div`
|
||||
background: #10163a;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LoginFormContainer = styled.div`
|
||||
min-height: 505px;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
export const Title = styled.h1`
|
||||
color: #ebeefd;
|
||||
font-size: 18px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
|
||||
export const SubTitle = styled.h2`
|
||||
color: #c2c6dc;
|
||||
font-size: 14px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
export const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const FormLabel = styled.label`
|
||||
color: #c2c6dc;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
margin-top: 14px;
|
||||
`;
|
||||
|
||||
export const FormTextInput = styled.input`
|
||||
width: 100%;
|
||||
background: #262c49;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
margin-top: 4px;
|
||||
padding: 0.7rem 1rem 0.7rem 3rem;
|
||||
font-size: 1rem;
|
||||
color: #c2c6dc;
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
export const FormIcon = styled.div`
|
||||
top: 30px;
|
||||
left: 16px;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
export const FormError = styled.span`
|
||||
font-size: 0.875rem;
|
||||
color: rgb(234, 84, 85);
|
||||
`;
|
||||
|
||||
export const LoginButton = styled(Button)``;
|
||||
|
||||
export const ActionButtons = styled.div`
|
||||
margin-top: 17.5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const RegisterButton = styled(Button)``;
|
||||
|
||||
export const LogoTitle = styled.div`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-left: 12px;
|
||||
transition: visibility, opacity, transform 0.25s ease;
|
||||
color: #7367f0;
|
||||
`;
|
||||
|
||||
export const LogoWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
color: rgb(222, 235, 255);
|
||||
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
|
||||
`;
|
150
frontend/src/shared/components/Register/index.tsx
Normal file
150
frontend/src/shared/components/Register/index.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import React, { useState } from 'react';
|
||||
import AccessAccount from 'shared/undraw/AccessAccount';
|
||||
import { User, Lock, Citadel } from 'shared/icons';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import {
|
||||
Form,
|
||||
LogoWrapper,
|
||||
LogoTitle,
|
||||
ActionButtons,
|
||||
RegisterButton,
|
||||
FormError,
|
||||
FormIcon,
|
||||
FormLabel,
|
||||
FormTextInput,
|
||||
Wrapper,
|
||||
Column,
|
||||
LoginFormWrapper,
|
||||
LoginFormContainer,
|
||||
Title,
|
||||
SubTitle,
|
||||
} from './Styles';
|
||||
|
||||
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 Register = ({ onSubmit }: RegisterProps) => {
|
||||
const [isComplete, setComplete] = useState(true);
|
||||
const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>();
|
||||
const loginSubmit = (data: RegisterFormData) => {
|
||||
setComplete(false);
|
||||
onSubmit(data, setComplete, setError);
|
||||
};
|
||||
return (
|
||||
<Wrapper>
|
||||
<Column>
|
||||
<AccessAccount width={275} height={250} />
|
||||
</Column>
|
||||
<Column>
|
||||
<LoginFormWrapper>
|
||||
<LoginFormContainer>
|
||||
<LogoWrapper>
|
||||
<Citadel width={42} height={42} />
|
||||
<LogoTitle>Citadel</LogoTitle>
|
||||
</LogoWrapper>
|
||||
<Title>Register</Title>
|
||||
<SubTitle>Please create the system admin user</SubTitle>
|
||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||
<FormLabel htmlFor="fullname">
|
||||
Full name
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="fullname"
|
||||
name="fullname"
|
||||
ref={register({ required: 'Full name is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="username">
|
||||
Username
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="email">
|
||||
Email
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
ref={register({
|
||||
required: 'Email is required',
|
||||
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
<FormLabel htmlFor="initials">
|
||||
Initials
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="initials"
|
||||
name="initials"
|
||||
ref={register({
|
||||
required: 'Initials is required',
|
||||
pattern: {
|
||||
value: INITIALS_PATTERN,
|
||||
message: 'Initials must be between 2 to 3 characters.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||
<FormLabel htmlFor="password">
|
||||
Password
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
<FormLabel htmlFor="password_confirm">
|
||||
Password (Confirm)
|
||||
<FormTextInput
|
||||
type="password"
|
||||
id="password_confirm"
|
||||
name="password_confirm"
|
||||
ref={register({ required: 'Password (confirm) is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock width={20} height={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
|
||||
|
||||
<ActionButtons>
|
||||
<RegisterButton type="submit" disabled={!isComplete}>
|
||||
Register
|
||||
</RegisterButton>
|
||||
</ActionButtons>
|
||||
</Form>
|
||||
</LoginFormContainer>
|
||||
</LoginFormWrapper>
|
||||
</Column>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
@ -263,13 +263,13 @@ const Settings: React.FC<SettingsProps> = ({ onProfileAvatarRemove, onProfileAva
|
||||
onProfileAvatarChange={onProfileAvatarChange}
|
||||
profile={profile.profileIcon}
|
||||
/>
|
||||
<Input value={profile.fullName} width="100%" label="Name" />
|
||||
<Input defaultValue={profile.fullName} width="100%" label="Name" />
|
||||
<Input
|
||||
value={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
||||
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
||||
width="100%"
|
||||
label="Initials "
|
||||
/>
|
||||
<Input value={profile.username ?? ''} width="100%" label="Username " />
|
||||
<Input defaultValue={profile.username ?? ''} width="100%" label="Username " />
|
||||
<Input width="100%" label="Email" />
|
||||
<Input width="100%" label="Bio" />
|
||||
<SettingActions>
|
||||
|
Reference in New Issue
Block a user