Compare commits
No commits in common. "master" and "0.3.2" have entirely different histories.
@ -10,8 +10,6 @@ windows:
|
|||||||
- yarn:
|
- yarn:
|
||||||
- cd frontend
|
- cd frontend
|
||||||
- yarn start
|
- yarn start
|
||||||
- worker:
|
|
||||||
- go run cmd/taskcafe/main.go worker
|
|
||||||
- web/editor:
|
- web/editor:
|
||||||
root: ./frontend
|
root: ./frontend
|
||||||
panes:
|
panes:
|
||||||
|
23
CHANGELOG.md
23
CHANGELOG.md
@ -4,26 +4,17 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## UNRELEASED
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- On login page, redirects to `/register` if no users exist (to help streamline initial setup)
|
- Task sorting & filtering
|
||||||
|
- Redesigned the Task Details UI
|
||||||
|
- Implement task group actions (duplicate/delete all tasks/sort)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixes new user popup form so that it can now be submitted
|
- removed CORS middleware to fix security issue
|
||||||
|
- Added 3 retries with backoff to initial database connection [(#47)](https://github.com/JordanKnott/taskcafe/issues/47)
|
||||||
## [0.3.5] - 2021-09-04
|
- Can now actually set a due date
|
||||||
|
|
||||||
### Added
|
|
||||||
- Project visibility can now be set to public - meaning anyone can view the project board
|
|
||||||
- When redirected to login page while trying to view a page that requires login, you'll be redirected back to the correct page after login
|
|
||||||
- When creating a new label within the LabelManager on a card, the new label will automatically be applied to the task after creation
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Switch primary font to Open Sans
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Any open popups are hidden when closing the Task Details window
|
|
||||||
|
|
||||||
## [0.1.1] - 2020-08-21
|
## [0.1.1] - 2020-08-21
|
||||||
|
|
||||||
|
@ -12,18 +12,24 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- taskcafe-postgres:/var/lib/postgresql/data
|
- taskcafe-postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 8865:5432
|
- 8855:5432
|
||||||
mailhog:
|
mailhog:
|
||||||
image: mailhog/mailhog:latest
|
image: mailhog/mailhog:latest
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 1025:1025
|
- 1025:1025
|
||||||
- 8025:8025
|
- 8025:8025
|
||||||
redis:
|
broker:
|
||||||
image: redis:6.2
|
image: rabbitmq:3-management
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 8060:15672
|
||||||
|
- 5672:5672
|
||||||
|
result_store:
|
||||||
|
image: memcached:1.6-alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 11211:11211
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
taskcafe-postgres:
|
taskcafe-postgres:
|
||||||
|
@ -12,9 +12,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
TASKCAFE_DATABASE_HOST: postgres
|
TASKCAFE_DATABASE_HOST: postgres
|
||||||
TASKCAFE_MIGRATE: "true"
|
TASKCAFE_MIGRATE: "true"
|
||||||
volumes:
|
|
||||||
- taskcafe-uploads:/root/uploads
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:12.3-alpine
|
image: postgres:12.3-alpine
|
||||||
restart: always
|
restart: always
|
||||||
@ -30,8 +27,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
taskcafe-postgres:
|
taskcafe-postgres:
|
||||||
external: false
|
external: false
|
||||||
taskcafe-uploads:
|
|
||||||
external: false
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
taskcafe-test:
|
taskcafe-test:
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
REACT_APP_ENABLE_POLLING=true
|
|
||||||
ESLINT_NO_DEV_ERRORS=true
|
|
@ -1 +0,0 @@
|
|||||||
REACT_APP_ENABLE_POLLING=false
|
|
@ -24,18 +24,15 @@
|
|||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "warn",
|
"prettier/prettier": "error",
|
||||||
"no-shadow": "off",
|
|
||||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
|
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
|
||||||
"react/require-default-props": "off",
|
|
||||||
"no-case-declarations": "off",
|
"no-case-declarations": "off",
|
||||||
"no-plusplus": "off",
|
"no-plusplus": "off",
|
||||||
"react/prop-types": 0,
|
"react/prop-types": 0,
|
||||||
"react/no-unused-prop-types": "off",
|
|
||||||
"no-continue": "off",
|
"no-continue": "off",
|
||||||
"react/jsx-props-no-spreading": "off",
|
"react/jsx-props-no-spreading": "off",
|
||||||
"no-param-reassign": "off",
|
"no-param-reassign": "off",
|
||||||
@ -50,8 +47,6 @@
|
|||||||
"tsx": "never"
|
"tsx": "never"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"no-use-before-define": "off",
|
|
||||||
"@typescript-eslint/no-use-before-define": ["error"],
|
|
||||||
"import/no-extraneous-dependencies": [
|
"import/no-extraneous-dependencies": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
schema:
|
schema:
|
||||||
- '../internal/graph/schema/*.gql'
|
- '../internal/graph/schema.graphqls'
|
||||||
documents:
|
documents:
|
||||||
- 'src/shared/graphql/*.graphqls'
|
- 'src/shared/graphql/*.graphqls'
|
||||||
- 'src/shared/graphql/**/*.ts'
|
- 'src/shared/graphql/**/*.ts'
|
||||||
|
@ -3,25 +3,28 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.16",
|
"@apollo/client": "^3.0.0-rc.8",
|
||||||
"@apollo/react-common": "^3.1.4",
|
"@apollo/react-common": "^3.1.4",
|
||||||
"@apollo/react-hooks": "^4.0.0",
|
"@apollo/react-hooks": "^3.1.3",
|
||||||
"@taskcafe/rich-markdown-editor": "^11.0.10",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/dompurify": "^2.2.2",
|
"@types/date-fns": "^2.6.0",
|
||||||
|
"@types/dompurify": "^2.0.4",
|
||||||
"@types/emoji-mart": "^3.0.4",
|
"@types/emoji-mart": "^3.0.4",
|
||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^24.0.0",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/jwt-decode": "^2.2.1",
|
||||||
"@types/node": "^15.0.1",
|
"@types/lodash": "^4.14.149",
|
||||||
"@types/react": "^17.0.20",
|
"@types/node": "^12.0.0",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/query-string": "^6.3.0",
|
||||||
"@types/react-datepicker": "^3.1.8",
|
"@types/react": "^16.9.21",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-beautiful-dnd": "^12.1.1",
|
||||||
"@types/react-router": "^5.1.13",
|
"@types/react-datepicker": "^2.11.0",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-dom": "^16.9.5",
|
||||||
"@types/react-select": "^4.0.15",
|
"@types/react-router": "^5.1.4",
|
||||||
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
"@types/react-select": "^3.0.13",
|
||||||
"@types/react-timeago": "^4.1.1",
|
"@types/react-timeago": "^4.1.1",
|
||||||
"@types/styled-components": "^5.1.0",
|
"@types/styled-components": "^5.0.0",
|
||||||
"apollo-cache-inmemory": "^1.6.5",
|
"apollo-cache-inmemory": "^1.6.5",
|
||||||
"apollo-client": "^2.6.8",
|
"apollo-client": "^2.6.8",
|
||||||
"apollo-link": "^1.2.13",
|
"apollo-link": "^1.2.13",
|
||||||
@ -30,42 +33,39 @@
|
|||||||
"apollo-link-state": "^0.4.2",
|
"apollo-link-state": "^0.4.2",
|
||||||
"apollo-utilities": "^1.3.3",
|
"apollo-utilities": "^1.3.3",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"axios-auth-refresh": "^3.1.0",
|
"axios-auth-refresh": "^2.2.7",
|
||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
"date-fns": "^2.21.1",
|
"date-fns": "^2.14.0",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.9.1",
|
||||||
"dompurify": "^2.2.8",
|
"dompurify": "^2.2.6",
|
||||||
"emoji-mart": "^3.0.1",
|
"emoji-mart": "^3.0.0",
|
||||||
"emoticon": "^4.0.0",
|
"emoticon": "^3.2.0",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.0.0",
|
||||||
"graphql-tag": "^2.12.4",
|
"graphql-tag": "^2.10.3",
|
||||||
"history": "^5.0.0",
|
"history": "^4.10.1",
|
||||||
"immer": "^9.0.2",
|
"immer": "^8.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^2.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.20",
|
||||||
"loglevel": "^1.7.1",
|
|
||||||
"loglevel-plugin-remote": "^0.6.8",
|
|
||||||
"node-emoji": "^1.10.0",
|
"node-emoji": "^1.10.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^6.13.7",
|
||||||
"react": "^17.0.2",
|
"react": "^16.12.0",
|
||||||
"react-autosize-textarea": "^7.0.0",
|
"react-autosize-textarea": "^7.0.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-datepicker": "^3.8.0",
|
"react-datepicker": "^2.14.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^16.12.0",
|
||||||
"react-emoji-render": "^1.2.4",
|
"react-emoji-render": "^1.2.4",
|
||||||
"react-hook-form": "^7.3.6",
|
"react-hook-form": "^6.0.6",
|
||||||
"react-markdown": "^6.0.1",
|
"react-markdown": "^4.3.1",
|
||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "3.4.0",
|
||||||
"react-select": "^4.3.0",
|
"react-select": "^3.1.0",
|
||||||
"react-timeago": "^5.2.0",
|
"react-timeago": "^4.4.0",
|
||||||
"react-toastify": "^7.0.4",
|
"react-toastify": "^6.0.8",
|
||||||
"rich-markdown-editor": "^11.17.4-0",
|
"rich-markdown-editor": "^10.6.5",
|
||||||
"styled-components": "^5.2.3",
|
"styled-components": "^5.0.1",
|
||||||
"typescript": "~4.2.4",
|
"typescript": "~3.7.2"
|
||||||
"unist-util-visit": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"proxy": "http://localhost:3333",
|
"proxy": "http://localhost:3333",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -93,20 +93,20 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^1.21.4",
|
"@graphql-codegen/cli": "^1.13.2",
|
||||||
"@graphql-codegen/typescript": "^1.22.0",
|
"@graphql-codegen/typescript": "^1.13.2",
|
||||||
"@graphql-codegen/typescript-operations": "^1.17.16",
|
"@graphql-codegen/typescript-operations": "^1.13.2",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^2.2.4",
|
"@graphql-codegen/typescript-react-apollo": "^1.13.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
"@typescript-eslint/eslint-plugin": "^2.20.0",
|
||||||
"@typescript-eslint/parser": "^4.22.0",
|
"@typescript-eslint/parser": "^2.20.0",
|
||||||
"eslint": "^7.25.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-airbnb": "^18.0.1",
|
"eslint-config-airbnb": "^18.0.1",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^6.10.0",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-import": "^2.20.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-plugin-prettier": "^3.1.2",
|
||||||
"eslint-plugin-react": "^7.23.2",
|
"eslint-plugin-react": "^7.18.3",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^1.7.0",
|
||||||
"prettier": "^2.2.1"
|
"prettier": "^1.19.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,17 +10,16 @@ import {
|
|||||||
UsersDocument,
|
UsersDocument,
|
||||||
UsersQuery,
|
UsersQuery,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
|
import Input from 'shared/components/Input';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { useForm, Controller, UseFormSetError } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import { Redirect } from 'react-router';
|
import { Redirect } from 'react-router';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import ControlledInput from 'shared/components/ControlledInput';
|
|
||||||
import FormInput from 'shared/components/FormInput';
|
|
||||||
|
|
||||||
const DeleteUserWrapper = styled.div`
|
const DeleteUserWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -78,56 +77,55 @@ const CreateUserButton = styled(Button)`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const AddUserInput = styled(FormInput)`
|
const AddUserInput = styled(Input)`
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InputError = styled.span`
|
const InputError = styled.span`
|
||||||
color: ${(props) => props.theme.colors.danger};
|
color: ${props => props.theme.colors.danger};
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type AddUserPopupProps = {
|
type AddUserPopupProps = {
|
||||||
onAddUser: (user: CreateUserData, setError: UseFormSetError<CreateUserData>) => void;
|
onAddUser: (user: CreateUserData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
|
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
|
||||||
const {
|
const { register, handleSubmit, errors, control } = useForm<CreateUserData>();
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
setError,
|
|
||||||
control,
|
|
||||||
} = useForm<CreateUserData>();
|
|
||||||
|
|
||||||
const createUser = (data: CreateUserData) => {
|
const createUser = (data: CreateUserData) => {
|
||||||
onAddUser(data, setError);
|
onAddUser(data);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<CreateUserForm onSubmit={handleSubmit(createUser)}>
|
<CreateUserForm onSubmit={handleSubmit(createUser)}>
|
||||||
<AddUserInput
|
<AddUserInput
|
||||||
|
floatingLabel
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Full Name"
|
label="Full Name"
|
||||||
|
id="fullName"
|
||||||
|
name="fullName"
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
{...register('fullName', { required: 'Full name is required' })}
|
ref={register({ required: 'Full name is required' })}
|
||||||
/>
|
/>
|
||||||
{errors.fullName && <InputError>{errors.fullName.message}</InputError>}
|
{errors.fullName && <InputError>{errors.fullName.message}</InputError>}
|
||||||
<AddUserInput
|
<AddUserInput
|
||||||
floatingLabel
|
floatingLabel
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Email"
|
label="Email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
{...register('email', { required: 'Email is required' })}
|
ref={register({ required: 'Email is required' })}
|
||||||
/>
|
/>
|
||||||
{errors.email && <InputError>{errors.email.message}</InputError>}
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="roleCode"
|
name="roleCode"
|
||||||
rules={{ required: 'Role is required' }}
|
rules={{ required: 'Role is required' }}
|
||||||
render={({ field }) => (
|
render={({ onChange, value }) => (
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
|
||||||
label="Role"
|
label="Role"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Admin', value: 'admin' },
|
{ label: 'Admin', value: 'admin' },
|
||||||
{ label: 'Member', value: 'member' },
|
{ label: 'Member', value: 'member' },
|
||||||
@ -140,25 +138,31 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
|
|||||||
floatingLabel
|
floatingLabel
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Username"
|
label="Username"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
{...register('username', { required: 'Username is required' })}
|
ref={register({ required: 'Username is required' })}
|
||||||
/>
|
/>
|
||||||
{errors.username && <InputError>{errors.username.message}</InputError>}
|
{errors.username && <InputError>{errors.username.message}</InputError>}
|
||||||
<AddUserInput
|
<AddUserInput
|
||||||
floatingLabel
|
floatingLabel
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Initials"
|
label="Initials"
|
||||||
|
id="initials"
|
||||||
|
name="initials"
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
{...register('initials', { required: 'Initials is required' })}
|
ref={register({ required: 'Initials is required' })}
|
||||||
/>
|
/>
|
||||||
{errors.initials && <InputError>{errors.initials.message}</InputError>}
|
{errors.initials && <InputError>{errors.initials.message}</InputError>}
|
||||||
<AddUserInput
|
<AddUserInput
|
||||||
floatingLabel
|
floatingLabel
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Password"
|
label="Password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
type="password"
|
type="password"
|
||||||
{...register('password', { required: 'Password is required' })}
|
ref={register({ required: 'Password is required' })}
|
||||||
/>
|
/>
|
||||||
{errors.password && <InputError>{errors.password.message}</InputError>}
|
{errors.password && <InputError>{errors.password.message}</InputError>}
|
||||||
<CreateUserButton type="submit">Create</CreateUserButton>
|
<CreateUserButton type="submit">Create</CreateUserButton>
|
||||||
@ -175,10 +179,10 @@ const AdminRoute = () => {
|
|||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
|
const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<UsersQuery>(client, UsersDocument, (cache) =>
|
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.invitedUsers = cache.invitedUsers.filter(
|
draftCache.invitedUsers = cache.invitedUsers.filter(
|
||||||
(u) => u.id !== response.data?.deleteInvitedUserAccount.invitedUser.id,
|
u => u.id !== response.data?.deleteInvitedUserAccount.invitedUser.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -186,9 +190,9 @@ const AdminRoute = () => {
|
|||||||
});
|
});
|
||||||
const [deleteUser] = useDeleteUserAccountMutation({
|
const [deleteUser] = useDeleteUserAccountMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<UsersQuery>(client, UsersDocument, (cache) =>
|
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.users = cache.users.filter((u) => u.id !== response.data?.deleteUserAccount.userAccount.id);
|
draftCache.users = cache.users.filter(u => u.id !== response.data?.deleteUserAccount.userAccount.id);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -211,12 +215,9 @@ const AdminRoute = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
/*
|
|
||||||
TODO: add permision check
|
|
||||||
if (user.roles.org !== 'admin') {
|
if (user.roles.org !== 'admin') {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
|
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
|
||||||
@ -224,13 +225,12 @@ TODO: add permision check
|
|||||||
initialTab={0}
|
initialTab={0}
|
||||||
users={data.users}
|
users={data.users}
|
||||||
invitedUsers={data.invitedUsers}
|
invitedUsers={data.invitedUsers}
|
||||||
// canInviteUser={user.roles.org === 'admin'} TODO: add permision check
|
canInviteUser={user.roles.org === 'admin'}
|
||||||
canInviteUser
|
|
||||||
onInviteUser={NOOP}
|
onInviteUser={NOOP}
|
||||||
onUpdateUserPassword={() => {
|
onUpdateUserPassword={() => {
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onDeleteInvitedUser={(invitedUserID) => {
|
onDeleteInvitedUser={invitedUserID => {
|
||||||
deleteInvitedUser({ variables: { invitedUserID } });
|
deleteInvitedUser({ variables: { invitedUserID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
@ -238,20 +238,15 @@ TODO: add permision check
|
|||||||
deleteUser({ variables: { userID, newOwnerID } });
|
deleteUser({ variables: { userID, newOwnerID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onAddUser={($target) => {
|
onAddUser={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
|
<Popup tab={0} title="Add member" onClose={() => hidePopup()}>
|
||||||
<AddUserPopup
|
<AddUserPopup
|
||||||
onAddUser={(u, setError) => {
|
onAddUser={u => {
|
||||||
const { roleCode, ...userData } = u;
|
const { roleCode, ...userData } = u;
|
||||||
createUser({
|
createUser({ variables: { ...userData, roleCode: roleCode.value } });
|
||||||
variables: { ...userData, roleCode: roleCode.value },
|
hidePopup();
|
||||||
})
|
|
||||||
.then(() => hidePopup())
|
|
||||||
.catch((e) => {
|
|
||||||
setError('email', { type: 'validate', message: e.message });
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Popup>,
|
</Popup>,
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
import { useGetProjectsQuery } from 'shared/generated/graphql';
|
import { useGetProjectsQuery } from 'shared/generated/graphql';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import LoadingSpinner from 'shared/components/LoadingSpinner';
|
import LoadingSpinner from 'shared/components/LoadingSpinner';
|
||||||
|
import theme from './ThemeStyles';
|
||||||
import ControlledInput from 'shared/components/ControlledInput';
|
import ControlledInput from 'shared/components/ControlledInput';
|
||||||
import { CaretDown, CaretRight } from 'shared/icons';
|
import { CaretDown, CaretRight } from 'shared/icons';
|
||||||
import useStickyState from 'shared/hooks/useStickyState';
|
import useStickyState from 'shared/hooks/useStickyState';
|
||||||
import { usePopup } from 'shared/components/PopupMenu';
|
import { usePopup } from 'shared/components/PopupMenu';
|
||||||
|
|
||||||
|
const colors = [theme.colors.primary, theme.colors.secondary];
|
||||||
|
|
||||||
const TeamContainer = styled.div`
|
const TeamContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -24,7 +27,6 @@ const TeamTitleText = styled.span`
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TeamProjects = styled.div`
|
const TeamProjects = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Switch, Route, useHistory, useLocation, Redirect } from 'react-router-dom';
|
import { Switch, Route, useHistory } from 'react-router-dom';
|
||||||
import * as H from 'history';
|
import * as H from 'history';
|
||||||
|
|
||||||
import Dashboard from 'Dashboard';
|
import Dashboard from 'Dashboard';
|
||||||
@ -13,6 +13,8 @@ import Login from 'Auth';
|
|||||||
import Register from 'Register';
|
import Register from 'Register';
|
||||||
import Profile from 'Profile';
|
import Profile from 'Profile';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import JwtDecode from 'jwt-decode';
|
||||||
|
import { setAccessToken } from 'shared/utils/accessToken';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
|
|
||||||
const MainContent = styled.div`
|
const MainContent = styled.div`
|
||||||
@ -24,64 +26,67 @@ const MainContent = styled.div`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ValidateTokenResponse = {
|
type RefreshTokenResponse = {
|
||||||
valid: boolean;
|
accessToken: string;
|
||||||
userID: string;
|
setup?: null | { confirmToken: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserRequiredRoute: React.FC<any> = ({ children }) => {
|
const AuthorizedRoutes = () => {
|
||||||
const { user } = useCurrentUser();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
|
||||||
if (user) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={{
|
|
||||||
pathname: '/login',
|
|
||||||
state: { redirect: location.pathname },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Routes: React.FC = () => {
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { setUser } = useCurrentUser();
|
const { setUser } = useCurrentUser();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/auth/validate', {
|
fetch('/auth/refresh_token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then(async (x) => {
|
}).then(async x => {
|
||||||
const response: ValidateTokenResponse = await x.json();
|
const { status } = x;
|
||||||
const { valid, userID } = response;
|
if (status === 400) {
|
||||||
if (valid) {
|
history.replace('/login');
|
||||||
setUser(userID);
|
} else {
|
||||||
|
const response: RefreshTokenResponse = await x.json();
|
||||||
|
const { accessToken, setup } = response;
|
||||||
|
if (setup) {
|
||||||
|
history.replace(`/register?confirmToken=${setup.confirmToken}`);
|
||||||
|
} else {
|
||||||
|
const claims: JWTToken = JwtDecode(accessToken);
|
||||||
|
const currentUser = {
|
||||||
|
id: claims.userId,
|
||||||
|
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||||
|
};
|
||||||
|
setUser(currentUser);
|
||||||
|
setAccessToken(accessToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
if (loading) return null;
|
return loading ? null : (
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/login" component={Login} />
|
|
||||||
<Route exact path="/register" component={Register} />
|
|
||||||
<Route exact path="/confirm" component={Confirm} />
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<MainContent>
|
<MainContent>
|
||||||
<Route path="/p/:projectID" component={Project} />
|
<Route exact path="/" component={Dashboard} />
|
||||||
|
<Route exact path="/projects" component={Projects} />
|
||||||
<UserRequiredRoute>
|
<Route path="/projects/:projectID" component={Project} />
|
||||||
<Route exact path="/" component={Projects} />
|
|
||||||
<Route path="/teams/:teamID" component={Teams} />
|
<Route path="/teams/:teamID" component={Teams} />
|
||||||
<Route path="/profile" component={Profile} />
|
<Route path="/profile" component={Profile} />
|
||||||
<Route path="/admin" component={Admin} />
|
<Route path="/admin" component={Admin} />
|
||||||
<Route path="/tasks" component={MyTasks} />
|
<Route path="/tasks" component={MyTasks} />
|
||||||
</UserRequiredRoute>
|
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Switch>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RoutesProps = {
|
||||||
|
history: H.History;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Routes: React.FC<RoutesProps> = () => (
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/login" component={Login} />
|
||||||
|
<Route exact path="/register" component={Register} />
|
||||||
|
<Route exact path="/confirm" component={Confirm} />
|
||||||
|
<AuthorizedRoutes />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
|
||||||
export default Routes;
|
export default Routes;
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
import { ToastContainer } from 'react-toastify';
|
|
||||||
|
|
||||||
const ToastedContainer = styled(ToastContainer).attrs({
|
|
||||||
// custom props
|
|
||||||
})`
|
|
||||||
.Toastify__toast-container {
|
|
||||||
}
|
|
||||||
.Toastify__toast {
|
|
||||||
padding: 5px;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #7367f0;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.Toastify__toast--error {
|
|
||||||
background: ${props => props.theme.colors.danger};
|
|
||||||
}
|
|
||||||
.Toastify__toast--warning {
|
|
||||||
background: ${props => props.theme.colors.warning};
|
|
||||||
}
|
|
||||||
.Toastify__toast--success {
|
|
||||||
background: ${props => props.theme.colors.success};
|
|
||||||
}
|
|
||||||
.Toastify__toast-body {
|
|
||||||
}
|
|
||||||
.Toastify__progress-bar {
|
|
||||||
}
|
|
||||||
.Toastify__close-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default ToastedContainer;
|
|
@ -1,24 +1,76 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
|
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
|
||||||
import LoggedOutNavbar from 'shared/components/TopNavbar/LoggedOut';
|
|
||||||
import { ProfileMenu } from 'shared/components/DropdownMenu';
|
import { ProfileMenu } from 'shared/components/DropdownMenu';
|
||||||
import polling from 'shared/utils/polling';
|
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||||
import { useHistory, useRouteMatch } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||||
import {
|
import {
|
||||||
RoleCode,
|
RoleCode,
|
||||||
useTopNavbarQuery,
|
useTopNavbarQuery,
|
||||||
useNotificationAddedSubscription,
|
useDeleteProjectMutation,
|
||||||
useHasUnreadNotificationsQuery,
|
GetProjectsDocument,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
|
import produce from 'immer';
|
||||||
import MiniProfile from 'shared/components/MiniProfile';
|
import MiniProfile from 'shared/components/MiniProfile';
|
||||||
import cache from 'App/cache';
|
import cache from 'App/cache';
|
||||||
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
|
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
|
||||||
import theme from 'App/ThemeStyles';
|
import theme from './ThemeStyles';
|
||||||
import ProjectFinder from './ProjectFinder';
|
import ProjectFinder from './ProjectFinder';
|
||||||
|
|
||||||
// TODO: Move to context based navbar?
|
type ProjectPopupProps = {
|
||||||
|
history: any;
|
||||||
|
name: string;
|
||||||
|
projectID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, projectID }) => {
|
||||||
|
const { hidePopup, setTab } = usePopup();
|
||||||
|
const [deleteProject] = useDeleteProjectMutation({
|
||||||
|
update: (client, deleteData) => {
|
||||||
|
const cacheData: any = client.readQuery({
|
||||||
|
query: GetProjectsDocument,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newData = produce(cacheData, (draftState: any) => {
|
||||||
|
draftState.projects = draftState.projects.filter(
|
||||||
|
(project: any) => project.id !== deleteData.data?.deleteProject.project.id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.writeQuery({
|
||||||
|
query: GetProjectsDocument,
|
||||||
|
data: {
|
||||||
|
...newData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popup title={null} tab={0}>
|
||||||
|
<ProjectSettings
|
||||||
|
onDeleteProject={() => {
|
||||||
|
setTab(1, { width: 300 });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popup>
|
||||||
|
<Popup title={`Delete the "${name}" project?`} tab={1}>
|
||||||
|
<DeleteConfirm
|
||||||
|
description={DELETE_INFO.DELETE_PROJECTS.description}
|
||||||
|
deletedItems={DELETE_INFO.DELETE_PROJECTS.deletedItems}
|
||||||
|
onConfirmDelete={() => {
|
||||||
|
if (projectID) {
|
||||||
|
deleteProject({ variables: { projectID } });
|
||||||
|
hidePopup();
|
||||||
|
history.push('/projects');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type GlobalTopNavbarProps = {
|
type GlobalTopNavbarProps = {
|
||||||
nameOnly?: boolean;
|
nameOnly?: boolean;
|
||||||
@ -39,7 +91,7 @@ type GlobalTopNavbarProps = {
|
|||||||
onRemoveInvitedFromBoard?: (email: string) => void;
|
onRemoveInvitedFromBoard?: (email: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
||||||
currentTab,
|
currentTab,
|
||||||
onSetTab,
|
onSetTab,
|
||||||
menuType,
|
menuType,
|
||||||
@ -55,33 +107,31 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
onRemoveInvitedFromBoard,
|
onRemoveInvitedFromBoard,
|
||||||
onRemoveFromBoard,
|
onRemoveFromBoard,
|
||||||
}) => {
|
}) => {
|
||||||
const [notifications, setNotifications] = useState<Array<{ id: string; notification: { actionType: string } }>>([]);
|
const { user, setUserRoles, setUser } = useCurrentUser();
|
||||||
const { data } = useTopNavbarQuery({
|
const { loading, data } = useTopNavbarQuery({
|
||||||
onCompleted: (d) => {
|
onCompleted: response => {
|
||||||
setNotifications((n) => [...n, ...d.notifications]);
|
if (user && user.roles) {
|
||||||
},
|
setUserRoles({
|
||||||
|
org: user.roles.org,
|
||||||
|
teams: response.me.teamRoles.reduce((map, obj) => {
|
||||||
|
map.set(obj.teamID, obj.roleCode);
|
||||||
|
return map;
|
||||||
|
}, new Map<string, string>()),
|
||||||
|
projects: response.me.projectRoles.reduce((map, obj) => {
|
||||||
|
map.set(obj.projectID, obj.roleCode);
|
||||||
|
return map;
|
||||||
|
}, new Map<string, string>()),
|
||||||
});
|
});
|
||||||
const { data: nData, loading } = useNotificationAddedSubscription({
|
|
||||||
onSubscriptionData: (d) => {
|
|
||||||
setNotifications((n) => {
|
|
||||||
if (d.subscriptionData.data) {
|
|
||||||
return [...n, d.subscriptionData.data.notificationAdded];
|
|
||||||
}
|
}
|
||||||
return n;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { setUser } = useCurrentUser();
|
|
||||||
const { data: unreadData, refetch: refetchHasUnread } = useHasUnreadNotificationsQuery({
|
|
||||||
pollInterval: polling.UNREAD_NOTIFICATIONS,
|
|
||||||
});
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
fetch('/auth/logout', {
|
fetch('/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then(async (x) => {
|
}).then(async x => {
|
||||||
const { status } = x;
|
const { status } = x;
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
cache.reset();
|
cache.reset();
|
||||||
@ -97,7 +147,7 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
<Popup title={null} tab={0}>
|
<Popup title={null} tab={0}>
|
||||||
<ProfileMenu
|
<ProfileMenu
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
showAdminConsole // TODO: add permision check
|
showAdminConsole={user ? user.roles.org === 'admin' : false}
|
||||||
onAdminConsole={() => {
|
onAdminConsole={() => {
|
||||||
history.push('/admin');
|
history.push('/admin');
|
||||||
hidePopup();
|
hidePopup();
|
||||||
@ -118,20 +168,30 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: rewrite popup to contain subscription and notification fetch
|
|
||||||
const onNotificationClick = ($target: React.RefObject<HTMLElement>) => {
|
const onNotificationClick = ($target: React.RefObject<HTMLElement>) => {
|
||||||
showPopup($target, <NotificationPopup onToggleRead={() => refetchHasUnread()} />, {
|
if (data) {
|
||||||
width: 605,
|
showPopup(
|
||||||
borders: false,
|
$target,
|
||||||
diamondColor: theme.colors.primary,
|
<NotificationPopup>
|
||||||
});
|
{data.notifications.map(notification => (
|
||||||
|
<NotificationItem
|
||||||
|
title={notification.entity.name}
|
||||||
|
description={`${notification.actor.name} added you as a meber to the task "${notification.entity.name}"`}
|
||||||
|
createdAt={notification.createdAt}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</NotificationPopup>,
|
||||||
|
{ width: 415, borders: false, diamondColor: theme.colors.primary },
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: readd permision check
|
if (!user) {
|
||||||
// const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
|
return null;
|
||||||
const userIsTeamOrProjectAdmin = true;
|
}
|
||||||
|
const userIsTeamOrProjectAdmin = user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID);
|
||||||
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
|
const onInvitedMemberProfile = ($targetRef: React.RefObject<HTMLElement>, email: string) => {
|
||||||
const member = projectInvitedMembers ? projectInvitedMembers.find((u) => u.email === email) : null;
|
const member = projectInvitedMembers ? projectInvitedMembers.find(u => u.email === email) : null;
|
||||||
if (member) {
|
if (member) {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
@ -159,7 +219,7 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
||||||
const member = projectMembers ? projectMembers.find((u) => u.id === memberID) : null;
|
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
|
||||||
const warning =
|
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”.';
|
'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”.';
|
||||||
if (member) {
|
if (member) {
|
||||||
@ -168,7 +228,7 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
<MiniProfile
|
<MiniProfile
|
||||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||||
canChangeRole={userIsTeamOrProjectAdmin}
|
canChangeRole={userIsTeamOrProjectAdmin}
|
||||||
onChangeRole={(roleCode) => {
|
onChangeRole={roleCode => {
|
||||||
if (onChangeRole) {
|
if (onChangeRole) {
|
||||||
onChangeRole(member.id, roleCode);
|
onChangeRole(member.id, roleCode);
|
||||||
}
|
}
|
||||||
@ -189,15 +249,12 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const user = data ? data.me?.user : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopNavbar
|
<TopNavbar
|
||||||
hasUnread={unreadData ? unreadData.hasUnreadNotifications.unread : false}
|
|
||||||
name={name}
|
name={name}
|
||||||
menuType={menuType}
|
menuType={menuType}
|
||||||
onOpenProjectFinder={($target) => {
|
onOpenProjectFinder={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<Popup tab={0} title={null}>
|
<Popup tab={0} title={null}>
|
||||||
@ -206,7 +263,7 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
user={user ?? null}
|
user={data ? data.me.user : null}
|
||||||
canEditProjectName={userIsTeamOrProjectAdmin}
|
canEditProjectName={userIsTeamOrProjectAdmin}
|
||||||
canInviteUser={userIsTeamOrProjectAdmin}
|
canInviteUser={userIsTeamOrProjectAdmin}
|
||||||
onMemberProfile={onMemberProfile}
|
onMemberProfile={onMemberProfile}
|
||||||
@ -233,46 +290,4 @@ const LoggedInNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
|
|
||||||
currentTab,
|
|
||||||
onSetTab,
|
|
||||||
menuType,
|
|
||||||
teamID,
|
|
||||||
onChangeProjectOwner,
|
|
||||||
onChangeRole,
|
|
||||||
name,
|
|
||||||
popupContent,
|
|
||||||
projectMembers,
|
|
||||||
projectInvitedMembers,
|
|
||||||
onInviteUser,
|
|
||||||
onSaveProjectName,
|
|
||||||
onRemoveInvitedFromBoard,
|
|
||||||
onRemoveFromBoard,
|
|
||||||
}) => {
|
|
||||||
const { user } = useCurrentUser();
|
|
||||||
const match = useRouteMatch();
|
|
||||||
if (user) {
|
|
||||||
return (
|
|
||||||
<LoggedInNavbar
|
|
||||||
currentTab={currentTab}
|
|
||||||
projectID={null}
|
|
||||||
onSetTab={onSetTab}
|
|
||||||
menuType={menuType}
|
|
||||||
teamID={teamID}
|
|
||||||
onChangeRole={onChangeRole}
|
|
||||||
onChangeProjectOwner={onChangeProjectOwner}
|
|
||||||
name={name}
|
|
||||||
popupContent={popupContent}
|
|
||||||
projectMembers={projectMembers}
|
|
||||||
projectInvitedMembers={projectInvitedMembers}
|
|
||||||
onInviteUser={onInviteUser}
|
|
||||||
onSaveProjectName={onSaveProjectName}
|
|
||||||
onRemoveInvitedFromBoard={onRemoveInvitedFromBoard}
|
|
||||||
onRemoveFromBoard={onRemoveFromBoard}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <LoggedOutNavbar match={match.url} name={name} menuType={menuType} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GlobalTopNavbar;
|
export default GlobalTopNavbar;
|
@ -1,89 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO, PublicConfirm } from 'shared/components/ProjectSettings';
|
|
||||||
import {
|
|
||||||
useDeleteProjectMutation,
|
|
||||||
GetProjectsDocument,
|
|
||||||
useToggleProjectVisibilityMutation,
|
|
||||||
} from 'shared/generated/graphql';
|
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
|
||||||
import produce from 'immer';
|
|
||||||
|
|
||||||
type ProjectPopupProps = {
|
|
||||||
history: any;
|
|
||||||
name: string;
|
|
||||||
publicOn: string | null;
|
|
||||||
projectID: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, projectID, publicOn: initialPublicOn }) => {
|
|
||||||
const { hidePopup, setTab } = usePopup();
|
|
||||||
const [publicOn, setPublicOn] = useState(initialPublicOn);
|
|
||||||
const [toggleProjectVisibility] = useToggleProjectVisibilityMutation({
|
|
||||||
onCompleted: data => {
|
|
||||||
setPublicOn(data.toggleProjectVisibility.project.publicOn);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [deleteProject] = useDeleteProjectMutation({
|
|
||||||
update: (client, deleteData) => {
|
|
||||||
const cacheData: any = client.readQuery({
|
|
||||||
query: GetProjectsDocument,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newData = produce(cacheData, (draftState: any) => {
|
|
||||||
draftState.projects = draftState.projects.filter(
|
|
||||||
(project: any) => project.id !== deleteData.data?.deleteProject.project.id,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.writeQuery({
|
|
||||||
query: GetProjectsDocument,
|
|
||||||
data: {
|
|
||||||
...newData,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Popup title={null} tab={0}>
|
|
||||||
<ProjectSettings
|
|
||||||
publicOn={publicOn}
|
|
||||||
onToggleProjectVisible={visible => {
|
|
||||||
if (visible) {
|
|
||||||
setTab(2, { width: 300 });
|
|
||||||
} else {
|
|
||||||
toggleProjectVisibility({ variables: { projectID, isPublic: false } });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDeleteProject={() => {
|
|
||||||
setTab(1, { width: 300 });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popup>
|
|
||||||
<Popup title="Change to public?" tab={1}>
|
|
||||||
<PublicConfirm
|
|
||||||
onConfirm={() => {
|
|
||||||
if (projectID) {
|
|
||||||
toggleProjectVisibility({ variables: { projectID, isPublic: true } });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popup>
|
|
||||||
<Popup title={`Delete the "${name}" project?`} tab={1}>
|
|
||||||
<DeleteConfirm
|
|
||||||
description={DELETE_INFO.DELETE_PROJECTS.description}
|
|
||||||
deletedItems={DELETE_INFO.DELETE_PROJECTS.deletedItems}
|
|
||||||
onConfirmDelete={() => {
|
|
||||||
if (projectID) {
|
|
||||||
deleteProject({ variables: { projectID } });
|
|
||||||
hidePopup();
|
|
||||||
history.push('/projects');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popup>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectPopup;
|
|
@ -1,4 +1,4 @@
|
|||||||
import { InMemoryCache } from '@apollo/client';
|
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||||
|
|
||||||
const cache = new InMemoryCache();
|
const cache = new InMemoryCache();
|
||||||
|
|
||||||
|
@ -1,20 +1,79 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
type UserContextState = {
|
export enum PermissionLevel {
|
||||||
user: string | null;
|
ORG,
|
||||||
setUser: (user: string | null) => void;
|
TEAM,
|
||||||
|
PROJECT,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PermissionObjectType {
|
||||||
|
ORG,
|
||||||
|
TEAM,
|
||||||
|
PROJECT,
|
||||||
|
TASK,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CurrentUserRoles = {
|
||||||
|
org: string;
|
||||||
|
teams: Map<string, string>;
|
||||||
|
projects: Map<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CurrentUserRaw {
|
||||||
|
id: string;
|
||||||
|
roles: CurrentUserRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserContextState = {
|
||||||
|
user: CurrentUserRaw | null;
|
||||||
|
setUser: (user: CurrentUserRaw | null) => void;
|
||||||
|
setUserRoles: (roles: CurrentUserRoles) => void;
|
||||||
|
};
|
||||||
export const UserContext = React.createContext<UserContextState>({
|
export const UserContext = React.createContext<UserContextState>({
|
||||||
user: null,
|
user: null,
|
||||||
setUser: _user => null,
|
setUser: _user => null,
|
||||||
|
setUserRoles: roles => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface CurrentUser extends CurrentUserRaw {
|
||||||
|
isAdmin: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||||
|
isVisible: (level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const useCurrentUser = () => {
|
export const useCurrentUser = () => {
|
||||||
const { user, setUser } = useContext(UserContext);
|
const { user, setUser, setUserRoles } = useContext(UserContext);
|
||||||
|
let currentUser: CurrentUser | null = null;
|
||||||
|
if (user) {
|
||||||
|
currentUser = {
|
||||||
|
...user,
|
||||||
|
isAdmin(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||||
|
if (user.roles.org === 'admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (level) {
|
||||||
|
case PermissionLevel.TEAM:
|
||||||
|
return subjectID ? this.roles.teams.get(subjectID) === 'admin' : false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isVisible(level: PermissionLevel, objectType: PermissionObjectType, subjectID?: string | null) {
|
||||||
|
if (user.roles.org === 'admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
switch (level) {
|
||||||
|
case PermissionLevel.TEAM:
|
||||||
|
return subjectID ? this.roles.teams.get(subjectID) !== null : false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
user,
|
user: currentUser,
|
||||||
setUser,
|
setUser,
|
||||||
|
setUserRoles,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url(../shared/fonts/OpenSans-Regular.ttf) format('truetype');
|
|
||||||
/* other formats include: 'woff2', 'truetype, 'opentype',
|
|
||||||
'embedded-opentype', and 'svg' */
|
|
||||||
}
|
|
@ -1,32 +1,75 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import jwtDecode from 'jwt-decode';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import { Router } from 'react-router';
|
||||||
import { PopupProvider } from 'shared/components/PopupMenu';
|
import { PopupProvider } from 'shared/components/PopupMenu';
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import { setAccessToken } from 'shared/utils/accessToken';
|
||||||
import styled, { ThemeProvider } from 'styled-components';
|
import styled, { ThemeProvider } from 'styled-components';
|
||||||
import NormalizeStyles from './NormalizeStyles';
|
import NormalizeStyles from './NormalizeStyles';
|
||||||
import BaseStyles from './BaseStyles';
|
import BaseStyles from './BaseStyles';
|
||||||
import theme from './ThemeStyles';
|
import theme from './ThemeStyles';
|
||||||
import Routes from './Routes';
|
import Routes from './Routes';
|
||||||
import ToastedContainer from './Toast';
|
import { UserContext, CurrentUserRaw, CurrentUserRoles, PermissionLevel, PermissionObjectType } from './context';
|
||||||
import { UserContext } from './context';
|
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import './fonts.css';
|
|
||||||
|
const StyledContainer = styled(ToastContainer).attrs({
|
||||||
|
// custom props
|
||||||
|
})`
|
||||||
|
.Toastify__toast-container {
|
||||||
|
}
|
||||||
|
.Toastify__toast {
|
||||||
|
padding: 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #7367f0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.Toastify__toast--error {
|
||||||
|
background: ${props => props.theme.colors.danger};
|
||||||
|
}
|
||||||
|
.Toastify__toast--warning {
|
||||||
|
background: ${props => props.theme.colors.warning};
|
||||||
|
}
|
||||||
|
.Toastify__toast--success {
|
||||||
|
background: ${props => props.theme.colors.success};
|
||||||
|
}
|
||||||
|
.Toastify__toast-body {
|
||||||
|
}
|
||||||
|
.Toastify__progress-bar {
|
||||||
|
}
|
||||||
|
.Toastify__close-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [user, setUser] = useState<string | null>(null);
|
const [user, setUser] = useState<CurrentUserRaw | null>(null);
|
||||||
|
const setUserRoles = (roles: CurrentUserRoles) => {
|
||||||
|
if (user) {
|
||||||
|
setUser({
|
||||||
|
...user,
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserContext.Provider value={{ user, setUser }}>
|
<UserContext.Provider value={{ user, setUser, setUserRoles }}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<NormalizeStyles />
|
<NormalizeStyles />
|
||||||
<BaseStyles />
|
<BaseStyles />
|
||||||
<BrowserRouter>
|
<Router history={history}>
|
||||||
<PopupProvider>
|
<PopupProvider>
|
||||||
<Routes />
|
<Routes history={history} />
|
||||||
</PopupProvider>
|
</PopupProvider>
|
||||||
</BrowserRouter>
|
</Router>
|
||||||
<ToastedContainer
|
<StyledContainer
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
autoClose={5000}
|
autoClose={5000}
|
||||||
hideProgressBar
|
hideProgressBar
|
||||||
|
@ -4,20 +4,10 @@ export const Container = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
position: relative;
|
|
||||||
top: 30%;
|
|
||||||
font-size: 150px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LoginWrapper = styled.div`
|
export const LoginWrapper = styled.div`
|
||||||
width: 70%;
|
width: 60%;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
width: 90%;
|
|
||||||
margin-top: 50vh;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { useHistory, useLocation } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import JwtDecode from 'jwt-decode';
|
||||||
|
import { setAccessToken } from 'shared/utils/accessToken';
|
||||||
import Login from 'shared/components/Login';
|
import Login from 'shared/components/Login';
|
||||||
import UserContext from 'App/context';
|
import UserContext from 'App/context';
|
||||||
import { Container, LoginWrapper } from './Styles';
|
import { Container, LoginWrapper } from './Styles';
|
||||||
@ -7,7 +9,6 @@ import { Container, LoginWrapper } from './Styles';
|
|||||||
const Auth = () => {
|
const Auth = () => {
|
||||||
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
|
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation<{ redirect: string } | undefined>();
|
|
||||||
const { setUser } = useContext(UserContext);
|
const { setUser } = useContext(UserContext);
|
||||||
const login = (
|
const login = (
|
||||||
data: LoginFormData,
|
data: LoginFormData,
|
||||||
@ -21,7 +22,7 @@ const Auth = () => {
|
|||||||
username: data.username,
|
username: data.username,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
}),
|
}),
|
||||||
}).then(async (x) => {
|
}).then(async x => {
|
||||||
if (x.status === 401) {
|
if (x.status === 401) {
|
||||||
setInvalidLoginAttempt(invalidLoginAttempt + 1);
|
setInvalidLoginAttempt(invalidLoginAttempt + 1);
|
||||||
setError('username', { type: 'error', message: 'Invalid username' });
|
setError('username', { type: 'error', message: 'Invalid username' });
|
||||||
@ -29,28 +30,43 @@ const Auth = () => {
|
|||||||
setComplete(true);
|
setComplete(true);
|
||||||
} else {
|
} else {
|
||||||
const response = await x.json();
|
const response = await x.json();
|
||||||
const { userID } = response;
|
const { accessToken } = response;
|
||||||
setUser(userID);
|
const claims: JWTToken = JwtDecode(accessToken);
|
||||||
if (location.state && location.state.redirect) {
|
const currentUser = {
|
||||||
history.push(location.state.redirect);
|
id: claims.userId,
|
||||||
} else {
|
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||||
|
};
|
||||||
|
setUser(currentUser);
|
||||||
|
setComplete(true);
|
||||||
|
setAccessToken(accessToken);
|
||||||
|
|
||||||
history.push('/');
|
history.push('/');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/auth/validate', {
|
fetch('/auth/refresh_token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then(async (x) => {
|
}).then(async x => {
|
||||||
const response = await x.json();
|
const { status } = x;
|
||||||
const { valid, userID } = response;
|
if (status === 200) {
|
||||||
if (valid) {
|
const response: RefreshTokenResponse = await x.json();
|
||||||
setUser(userID);
|
const { accessToken, setup } = response;
|
||||||
|
if (setup) {
|
||||||
|
history.replace(`/register?confirmToken=${setup.confirmToken}`);
|
||||||
|
} else {
|
||||||
|
const claims: JWTToken = JwtDecode(accessToken);
|
||||||
|
const currentUser = {
|
||||||
|
id: claims.userId,
|
||||||
|
roles: { org: claims.orgRole, teams: new Map<string, string>(), projects: new Map<string, string>() },
|
||||||
|
};
|
||||||
|
setUser(currentUser);
|
||||||
|
setAccessToken(accessToken);
|
||||||
history.replace('/projects');
|
history.replace('/projects');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -1,42 +1,58 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
import Confirm from 'shared/components/Confirm';
|
import Confirm from 'shared/components/Confirm';
|
||||||
import { useHistory, useLocation } from 'react-router';
|
import { useHistory, useLocation } from 'react-router';
|
||||||
import * as QueryString from 'query-string';
|
import * as QueryString from 'query-string';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { toast } from 'react-toastify';
|
||||||
import { Container, LoginWrapper } from './Styles';
|
import { Container, LoginWrapper } from './Styles';
|
||||||
|
import JwtDecode from 'jwt-decode';
|
||||||
|
import { setAccessToken } from 'shared/utils/accessToken';
|
||||||
|
import { useCurrentUser } from 'App/context';
|
||||||
|
|
||||||
const UsersConfirm = () => {
|
const UsersConfirm = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [registered, setRegistered] = useState(false);
|
||||||
const params = QueryString.parse(location.search);
|
const params = QueryString.parse(location.search);
|
||||||
const [hasFailed, setFailed] = useState(false);
|
|
||||||
const { setUser } = useCurrentUser();
|
const { setUser } = useCurrentUser();
|
||||||
useEffect(() => {
|
return (
|
||||||
|
<Container>
|
||||||
|
<LoginWrapper>
|
||||||
|
<Confirm
|
||||||
|
hasConfirmToken={params.confirmToken !== undefined}
|
||||||
|
onConfirmUser={setFailed => {
|
||||||
fetch('/auth/confirm', {
|
fetch('/auth/confirm', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
confirmToken: params.confirmToken,
|
confirmToken: params.confirmToken,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(async (x) => {
|
.then(async x => {
|
||||||
const { status } = x;
|
const { status } = x;
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
const response = await x.json();
|
const response = await x.json();
|
||||||
const { userID } = response;
|
const { accessToken } = response;
|
||||||
setUser(userID);
|
const claims: JWTToken = JwtDecode(accessToken);
|
||||||
|
const currentUser = {
|
||||||
|
id: claims.userId,
|
||||||
|
roles: {
|
||||||
|
org: claims.orgRole,
|
||||||
|
teams: new Map<string, string>(),
|
||||||
|
projects: new Map<string, string>(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setUser(currentUser);
|
||||||
|
setAccessToken(accessToken);
|
||||||
history.push('/');
|
history.push('/');
|
||||||
} else {
|
} else {
|
||||||
setFailed(true);
|
setFailed();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setFailed(false);
|
setFailed();
|
||||||
});
|
});
|
||||||
}, []);
|
}}
|
||||||
return (
|
/>
|
||||||
<Container>
|
|
||||||
<LoginWrapper>
|
|
||||||
<Confirm hasConfirmToken={params.confirmToken !== undefined} hasFailed={hasFailed} />
|
|
||||||
</LoginWrapper>
|
</LoginWrapper>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -26,7 +26,6 @@ import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
|||||||
import DueDateManager from 'shared/components/DueDateManager';
|
import DueDateManager from 'shared/components/DueDateManager';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import useStickyState from 'shared/hooks/useStickyState';
|
import useStickyState from 'shared/hooks/useStickyState';
|
||||||
import { StaticContext } from 'react-router';
|
|
||||||
import MyTasksSortPopup from './MyTasksSort';
|
import MyTasksSortPopup from './MyTasksSort';
|
||||||
import MyTasksStatusPopup from './MyTasksStatus';
|
import MyTasksStatusPopup from './MyTasksStatus';
|
||||||
import TaskEntry from './TaskEntry';
|
import TaskEntry from './TaskEntry';
|
||||||
@ -62,7 +61,11 @@ function prettySort(sort: MyTasksSort) {
|
|||||||
if (sort === MyTasksSort.None) {
|
if (sort === MyTasksSort.None) {
|
||||||
return 'Sort';
|
return 'Sort';
|
||||||
}
|
}
|
||||||
return `Sort: ${sort.charAt(0) + sort.slice(1).toLowerCase().replace(/_/gi, ' ')}`;
|
return `Sort: ${sort.charAt(0) +
|
||||||
|
sort
|
||||||
|
.slice(1)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/_/gi, ' ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group = {
|
type Group = {
|
||||||
@ -72,7 +75,7 @@ type Group = {
|
|||||||
};
|
};
|
||||||
const DueDateEditorLabel = styled.div`
|
const DueDateEditorLabel = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
@ -104,16 +107,16 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
${(props) =>
|
${props =>
|
||||||
props.disabled &&
|
props.disabled &&
|
||||||
css`
|
css`
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -147,7 +150,7 @@ const ProjectAction: React.FC<ProjectActionProps> = ({ onClick, disabled = false
|
|||||||
|
|
||||||
const EditorPositioner = styled.div<{ top: number; left: number }>`
|
const EditorPositioner = styled.div<{ top: number; left: number }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${(p) => p.top}px;
|
top: ${p => p.top}px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-left: -100vw;
|
margin-left: -100vw;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
@ -157,7 +160,7 @@ const EditorPositioner = styled.div<{ top: number; left: number }>`
|
|||||||
height: 0;
|
height: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
left: ${(p) => p.left}px;
|
left: ${p => p.left}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditorPositionerContents = styled.div`
|
const EditorPositionerContents = styled.div`
|
||||||
@ -165,15 +168,15 @@ const EditorPositionerContents = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const EditorContainer = styled.div<{ width: number }>`
|
const EditorContainer = styled.div<{ width: number }>`
|
||||||
border: 1px solid ${(props) => props.theme.colors.primary};
|
border: 1px solid ${props => props.theme.colors.primary};
|
||||||
background: ${(props) => props.theme.colors.bg.secondary};
|
background: ${props => props.theme.colors.bg.secondary};
|
||||||
position: relative;
|
position: relative;
|
||||||
width: ${(p) => p.width}px;
|
width: ${p => p.width}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EditorCell = styled.div<{ width: number }>`
|
const EditorCell = styled.div<{ width: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
width: ${(p) => p.width}px;
|
width: ${p => p.width}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TABLE
|
// TABLE
|
||||||
@ -221,7 +224,7 @@ const TaskGroupItems = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ProjectPill = styled.div`
|
const ProjectPill = styled.div`
|
||||||
background-color: ${(props) => props.theme.colors.bg.primary};
|
background-color: ${props => props.theme.colors.bg.primary};
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -247,7 +250,7 @@ const ProjectPillName = styled.span`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ProjectPillColor = styled.svg`
|
const ProjectPillColor = styled.svg`
|
||||||
@ -296,7 +299,7 @@ const OptionTitle = styled.div`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
const OptionSubTitle = styled.div`
|
const OptionSubTitle = styled.div`
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
@ -316,7 +319,7 @@ const Option = ({ innerProps, data }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TaskGroupHeaderContents = styled.div<{ width: number }>`
|
const TaskGroupHeaderContents = styled.div<{ width: number }>`
|
||||||
width: ${(p) => p.width}px;
|
width: ${p => p.width}px;
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
@ -353,13 +356,13 @@ const TaskGroupMinify = styled.div`
|
|||||||
transition-property: background, border, box-shadow, fill;
|
transition-property: background, border, box-shadow, fill;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => props.theme.colors.text.primary};
|
fill: ${props => props.theme.colors.text.primary};
|
||||||
transition-duration: 0.2s;
|
transition-duration: 0.2s;
|
||||||
transition-property: background, border, box-shadow, fill;
|
transition-property: background, border, box-shadow, fill;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover svg {
|
&:hover svg {
|
||||||
fill: ${(props) => props.theme.colors.text.secondary};
|
fill: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const TaskGroupName = styled.div`
|
const TaskGroupName = styled.div`
|
||||||
@ -368,7 +371,7 @@ const TaskGroupName = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -390,7 +393,7 @@ const Row = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const RowHeaderLeft = styled.div<{ width: number }>`
|
const RowHeaderLeft = styled.div<{ width: number }>`
|
||||||
width: ${(p) => p.width}px;
|
width: ${p => p.width}px;
|
||||||
|
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -402,7 +405,7 @@ const RowHeaderLeft = styled.div<{ width: number }>`
|
|||||||
`;
|
`;
|
||||||
const RowHeaderLeftInner = styled.div`
|
const RowHeaderLeftInner = styled.div`
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -426,7 +429,7 @@ const RowHeaderLeftNameText = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const RowHeaderRight = styled.div<{ left: number }>`
|
const RowHeaderRight = styled.div<{ left: number }>`
|
||||||
left: ${(p) => p.left}px;
|
left: ${p => p.left}px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
height: 37px;
|
height: 37px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -458,7 +461,7 @@ const RowHeaderRightContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ItemWrapper = styled.div<{ width: number }>`
|
const ItemWrapper = styled.div<{ width: number }>`
|
||||||
width: ${(p) => p.width}px;
|
width: ${p => p.width}px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid #414561;
|
border: 1px solid #414561;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
@ -471,11 +474,11 @@ const ItemWrapper = styled.div<{ width: number }>`
|
|||||||
margin-right: -1px;
|
margin-right: -1px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
border-bottom: 1px solid #414561;
|
border-bottom: 1px solid #414561;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const ItemsContainer = styled.div`
|
const ItemsContainer = styled.div`
|
||||||
@ -562,37 +565,14 @@ const Projects = () => {
|
|||||||
onCancel={() => null}
|
onCancel={() => null}
|
||||||
onDueDateChange={(task, dueDate, hasTime) => {
|
onDueDateChange={(task, dueDate, hasTime) => {
|
||||||
if (dateEditor.task) {
|
if (dateEditor.task) {
|
||||||
hidePopup();
|
updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } });
|
||||||
updateTaskDueDate({
|
setDateEditor(prev => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } }));
|
||||||
variables: {
|
|
||||||
taskID: dateEditor.task.id,
|
|
||||||
dueDate,
|
|
||||||
hasTime,
|
|
||||||
deleteNotifications: [],
|
|
||||||
updateNotifications: [],
|
|
||||||
createNotifications: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setDateEditor((prev) => ({
|
|
||||||
...prev,
|
|
||||||
task: { ...task, dueDate: { at: dueDate.toISOString(), notifications: [] }, hasTime },
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onRemoveDueDate={(task) => {
|
onRemoveDueDate={task => {
|
||||||
if (dateEditor.task) {
|
if (dateEditor.task) {
|
||||||
hidePopup();
|
updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } });
|
||||||
updateTaskDueDate({
|
setDateEditor(prev => ({ ...prev, task: { ...task, hasTime: false } }));
|
||||||
variables: {
|
|
||||||
taskID: dateEditor.task.id,
|
|
||||||
dueDate: null,
|
|
||||||
hasTime: false,
|
|
||||||
deleteNotifications: [],
|
|
||||||
updateNotifications: [],
|
|
||||||
createNotifications: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setDateEditor((prev) => ({ ...prev, task: { ...task, hasTime: false } }));
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -607,8 +587,8 @@ const Projects = () => {
|
|||||||
updateApolloCache<MyTasksQuery>(
|
updateApolloCache<MyTasksQuery>(
|
||||||
client,
|
client,
|
||||||
MyTasksDocument,
|
MyTasksDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (newTaskData.data) {
|
if (newTaskData.data) {
|
||||||
draftCache.myTasks.tasks.unshift(newTaskData.data.createTask);
|
draftCache.myTasks.tasks.unshift(newTaskData.data.createTask);
|
||||||
}
|
}
|
||||||
@ -638,7 +618,7 @@ const Projects = () => {
|
|||||||
groups.push({
|
groups.push({
|
||||||
id: 'recently-assigned',
|
id: 'recently-assigned',
|
||||||
name: 'Recently Assigned',
|
name: 'Recently Assigned',
|
||||||
tasks: data.myTasks.tasks.map((task) => ({
|
tasks: data.myTasks.tasks.map(task => ({
|
||||||
...task,
|
...task,
|
||||||
labels: [],
|
labels: [],
|
||||||
position: 0,
|
position: 0,
|
||||||
@ -648,27 +628,27 @@ const Projects = () => {
|
|||||||
let { tasks } = data.myTasks;
|
let { tasks } = data.myTasks;
|
||||||
if (filters.sort === MyTasksSort.DueDate) {
|
if (filters.sort === MyTasksSort.DueDate) {
|
||||||
const group: Group = { id: 'due_date', name: null, tasks: [] };
|
const group: Group = { id: 'due_date', name: null, tasks: [] };
|
||||||
data.myTasks.tasks.forEach((task) => {
|
data.myTasks.tasks.forEach(task => {
|
||||||
if (task.dueDate) {
|
if (task.dueDate) {
|
||||||
group.tasks.push({ ...task, labels: [], position: 0 });
|
group.tasks.push({ ...task, labels: [], position: 0 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
groups.push(group);
|
groups.push(group);
|
||||||
tasks = tasks.filter((t) => t.dueDate === null);
|
tasks = tasks.filter(t => t.dueDate === null);
|
||||||
}
|
}
|
||||||
const projects = new Map<string, Array<Task>>();
|
const projects = new Map<string, Array<Task>>();
|
||||||
data.myTasks.projects.forEach((p) => {
|
data.myTasks.projects.forEach(p => {
|
||||||
if (!projects.has(p.projectID)) {
|
if (!projects.has(p.projectID)) {
|
||||||
projects.set(p.projectID, []);
|
projects.set(p.projectID, []);
|
||||||
}
|
}
|
||||||
const prev = projects.get(p.projectID);
|
const prev = projects.get(p.projectID);
|
||||||
const task = tasks.find((t) => t.id === p.taskID);
|
const task = tasks.find(t => t.id === p.taskID);
|
||||||
if (prev && task) {
|
if (prev && task) {
|
||||||
projects.set(p.projectID, [...prev, { ...task, labels: [], position: 0 }]);
|
projects.set(p.projectID, [...prev, { ...task, labels: [], position: 0 }]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (const [id, pTasks] of projects) {
|
for (const [id, pTasks] of projects) {
|
||||||
const project = data.projects.find((c) => c.id === id);
|
const project = data.projects.find(c => c.id === id);
|
||||||
if (pTasks.length === 0) continue;
|
if (pTasks.length === 0) continue;
|
||||||
if (project) {
|
if (project) {
|
||||||
groups.push({
|
groups.push({
|
||||||
@ -678,8 +658,8 @@ const Projects = () => {
|
|||||||
if (a.dueDate === null && b.dueDate === null) return 0;
|
if (a.dueDate === null && b.dueDate === null) return 0;
|
||||||
if (a.dueDate === null && b.dueDate !== null) return 1;
|
if (a.dueDate === null && b.dueDate !== null) return 1;
|
||||||
if (a.dueDate !== null && b.dueDate === null) return -1;
|
if (a.dueDate !== null && b.dueDate === null) return -1;
|
||||||
const first = dayjs(a.dueDate.at);
|
const first = dayjs(a.dueDate);
|
||||||
const second = dayjs(b.dueDate.at);
|
const second = dayjs(b.dueDate);
|
||||||
if (first.isSame(second, 'minute')) return 0;
|
if (first.isSame(second, 'minute')) return 0;
|
||||||
if (first.isAfter(second)) return -1;
|
if (first.isAfter(second)) return -1;
|
||||||
return 1;
|
return 1;
|
||||||
@ -701,13 +681,13 @@ const Projects = () => {
|
|||||||
<ProjectActions />
|
<ProjectActions />
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<MyTasksStatusPopup
|
<MyTasksStatusPopup
|
||||||
status={filters.status}
|
status={filters.status}
|
||||||
onChangeStatus={(status) => {
|
onChangeStatus={status => {
|
||||||
setFilters((prev) => ({ ...prev, status }));
|
setFilters(prev => ({ ...prev, status }));
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
@ -719,13 +699,13 @@ const Projects = () => {
|
|||||||
<ProjectActionText>{prettyStatus(filters.status)}</ProjectActionText>
|
<ProjectActionText>{prettyStatus(filters.status)}</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<MyTasksSortPopup
|
<MyTasksSortPopup
|
||||||
sort={filters.sort}
|
sort={filters.sort}
|
||||||
onChangeSort={(sort) => {
|
onChangeSort={sort => {
|
||||||
setFilters((prev) => ({ ...prev, sort }));
|
setFilters(prev => ({ ...prev, sort }));
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
@ -772,8 +752,8 @@ const Projects = () => {
|
|||||||
<VerticalScoller>
|
<VerticalScoller>
|
||||||
<VerticalScollerInner>
|
<VerticalScollerInner>
|
||||||
<TableContents>
|
<TableContents>
|
||||||
{groups.map((group) => {
|
{groups.map(group => {
|
||||||
const isMinified = minified.find((m) => m === group.id) ?? false;
|
const isMinified = minified.find(m => m === group.id) ?? false;
|
||||||
return (
|
return (
|
||||||
<TaskGroupContainer key={group.id}>
|
<TaskGroupContainer key={group.id}>
|
||||||
{group.name && (
|
{group.name && (
|
||||||
@ -781,9 +761,9 @@ const Projects = () => {
|
|||||||
<TaskGroupHeaderContents width={leftRow}>
|
<TaskGroupHeaderContents width={leftRow}>
|
||||||
<TaskGroupMinify
|
<TaskGroupMinify
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMinified((prev) => {
|
setMinified(prev => {
|
||||||
if (isMinified) {
|
if (isMinified) {
|
||||||
return prev.filter((c) => c !== group.id);
|
return prev.filter(c => c !== group.id);
|
||||||
}
|
}
|
||||||
return [...prev, group.id];
|
return [...prev, group.id];
|
||||||
});
|
});
|
||||||
@ -801,40 +781,29 @@ const Projects = () => {
|
|||||||
)}
|
)}
|
||||||
<TaskGroupItems>
|
<TaskGroupItems>
|
||||||
{!isMinified &&
|
{!isMinified &&
|
||||||
group.tasks.map((task) => {
|
group.tasks.map(task => {
|
||||||
const projectID = data.myTasks.projects.find((t) => t.taskID === task.id)?.projectID;
|
const projectID = data.myTasks.projects.find(t => t.taskID === task.id)?.projectID;
|
||||||
const projectName = data.projects.find((p) => p.id === projectID)?.name;
|
const projectName = data.projects.find(p => p.id === projectID)?.name;
|
||||||
return (
|
return (
|
||||||
<TaskEntry
|
<TaskEntry
|
||||||
key={task.id}
|
key={task.id}
|
||||||
complete={task.complete ?? false}
|
complete={task.complete ?? false}
|
||||||
onToggleComplete={(complete) => {
|
onToggleComplete={complete => {
|
||||||
setTaskComplete({ variables: { taskID: task.id, complete } });
|
setTaskComplete({ variables: { taskID: task.id, complete } });
|
||||||
}}
|
}}
|
||||||
onTaskDetails={() => {
|
onTaskDetails={() => {
|
||||||
history.push(`${match.url}/c/${task.id}`);
|
history.push(`${match.url}/c/${task.id}`);
|
||||||
}}
|
}}
|
||||||
onRemoveDueDate={() => {
|
onRemoveDueDate={() => {
|
||||||
updateTaskDueDate({
|
updateTaskDueDate({ variables: { taskID: task.id, dueDate: null, hasTime: false } });
|
||||||
variables: {
|
|
||||||
taskID: task.id,
|
|
||||||
dueDate: null,
|
|
||||||
hasTime: false,
|
|
||||||
deleteNotifications: [],
|
|
||||||
updateNotifications: [],
|
|
||||||
createNotifications: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
project={projectName ?? 'none'}
|
project={projectName ?? 'none'}
|
||||||
dueDate={task.dueDate.at}
|
dueDate={task.dueDate}
|
||||||
hasTime={task.hasTime ?? false}
|
hasTime={task.hasTime ?? false}
|
||||||
name={task.name}
|
name={task.name}
|
||||||
onEditName={(name) => updateTaskName({ variables: { taskID: task.id, name } })}
|
onEditName={name => updateTaskName({ variables: { taskID: task.id, name } })}
|
||||||
onEditProject={onEditProject}
|
onEditProject={onEditProject}
|
||||||
onEditDueDate={($target) =>
|
onEditDueDate={$target => onEditDueDate({ ...task, position: 0, labels: [] }, $target)}
|
||||||
onEditDueDate({ ...task, position: 0, labels: [] }, $target)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -853,9 +822,7 @@ const Projects = () => {
|
|||||||
<EditorCell width={120}>
|
<EditorCell width={120}>
|
||||||
<DueDateEditorLabel>
|
<DueDateEditorLabel>
|
||||||
{dateEditor.task.dueDate
|
{dateEditor.task.dueDate
|
||||||
? dayjs(dateEditor.task.dueDate.at).format(
|
? dayjs(dateEditor.task.dueDate).format(dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D')
|
||||||
dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D',
|
|
||||||
)
|
|
||||||
: ''}
|
: ''}
|
||||||
</DueDateEditorLabel>
|
</DueDateEditorLabel>
|
||||||
</EditorCell>
|
</EditorCell>
|
||||||
@ -889,12 +856,12 @@ const Projects = () => {
|
|||||||
)}
|
)}
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}/c/:taskID`}
|
path={`${match.path}/c/:taskID`}
|
||||||
render={() => {
|
render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
|
||||||
return (
|
|
||||||
<Details
|
<Details
|
||||||
refreshCache={NOOP}
|
refreshCache={NOOP}
|
||||||
availableMembers={[]}
|
availableMembers={[]}
|
||||||
projectURL={`${match.url}`}
|
projectURL={`${match.url}`}
|
||||||
|
taskID={routeProps.match.params.taskID}
|
||||||
onTaskNameChange={(updatedTask, newName) => {
|
onTaskNameChange={(updatedTask, newName) => {
|
||||||
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
|
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
|
||||||
}}
|
}}
|
||||||
@ -913,7 +880,7 @@ const Projects = () => {
|
|||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
}}
|
}}
|
||||||
onDeleteTask={(deletedTask) => {
|
onDeleteTask={deletedTask => {
|
||||||
// deleteTask({ variables: { taskID: deletedTask.id } });
|
// deleteTask({ variables: { taskID: deletedTask.id } });
|
||||||
history.push(`${match.url}`);
|
history.push(`${match.url}`);
|
||||||
}}
|
}}
|
||||||
@ -935,8 +902,7 @@ const Projects = () => {
|
|||||||
*/
|
*/
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
|
import { getAccessToken } from 'shared/utils/accessToken';
|
||||||
import Settings from 'shared/components/Settings';
|
import Settings from 'shared/components/Settings';
|
||||||
import {
|
import {
|
||||||
useMeQuery,
|
useMeQuery,
|
||||||
@ -44,15 +45,18 @@ const Projects = () => {
|
|||||||
name="file"
|
name="file"
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
ref={$fileUpload}
|
ref={$fileUpload}
|
||||||
onChange={(e) => {
|
onChange={e => {
|
||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
const fileData = new FormData();
|
const fileData = new FormData();
|
||||||
fileData.append('file', e.target.files[0]);
|
fileData.append('file', e.target.files[0]);
|
||||||
|
const accessToken = getAccessToken();
|
||||||
axios
|
axios
|
||||||
.post('/users/me/avatar', fileData, {
|
.post('/users/me/avatar', fileData, {
|
||||||
withCredentials: true,
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then(res => {
|
||||||
if ($fileUpload && $fileUpload.current) {
|
if ($fileUpload && $fileUpload.current) {
|
||||||
$fileUpload.current.value = '';
|
$fileUpload.current.value = '';
|
||||||
refetch();
|
refetch();
|
||||||
@ -62,7 +66,7 @@ const Projects = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
|
<GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />
|
||||||
{!loading && data && data.me && (
|
{!loading && data && (
|
||||||
<Settings
|
<Settings
|
||||||
profile={data.me.user}
|
profile={data.me.user}
|
||||||
onProfileAvatarChange={() => {
|
onProfileAvatarChange={() => {
|
||||||
@ -71,13 +75,13 @@ const Projects = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onResetPassword={(password, done) => {
|
onResetPassword={(password, done) => {
|
||||||
updateUserPassword({ variables: { userID: user, password } });
|
updateUserPassword({ variables: { userID: user.id, password } });
|
||||||
toast('Password was changed!');
|
toast('Password was changed!');
|
||||||
done();
|
done();
|
||||||
}}
|
}}
|
||||||
onChangeUserInfo={(d, done) => {
|
onChangeUserInfo={(d, done) => {
|
||||||
updateUserInfo({
|
updateUserInfo({
|
||||||
variables: { name: d.fullName, bio: d.bio, email: d.email, initials: d.initials },
|
variables: { name: d.full_name, bio: d.bio, email: d.email, initials: d.initials },
|
||||||
});
|
});
|
||||||
toast('User info was saved!');
|
toast('User info was saved!');
|
||||||
done();
|
done();
|
||||||
|
@ -7,13 +7,12 @@ import { Popup, usePopup } from 'shared/components/PopupMenu';
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
import Member from 'shared/components/Member';
|
import Member from 'shared/components/Member';
|
||||||
import { useLabelsQuery } from 'shared/generated/graphql';
|
|
||||||
|
|
||||||
const FilterMember = styled(Member)`
|
const FilterMember = styled(Member)`
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -29,7 +28,7 @@ export const Label = styled.li`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CardLabel = styled.span<{ active: boolean; color: string }>`
|
export const CardLabel = styled.span<{ active: boolean; color: string }>`
|
||||||
${(props) =>
|
${props =>
|
||||||
props.active &&
|
props.active &&
|
||||||
css`
|
css`
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
@ -44,7 +43,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: padding 85ms, margin 85ms, box-shadow 85ms;
|
transition: padding 85ms, margin 85ms, box-shadow 85ms;
|
||||||
background-color: ${(props) => props.color};
|
background-color: ${props => props.color};
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -72,7 +71,7 @@ export const ActionItem = styled.li`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -81,7 +80,7 @@ export const ActionTitle = styled.span`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ActionItemSeparator = styled.li`
|
const ActionItemSeparator = styled.li`
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
@ -108,25 +107,18 @@ const ActionItemLine = styled.div`
|
|||||||
margin: 0.25rem !important;
|
margin: 0.25rem !important;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ControlFilterProps = {
|
type FilterMetaProps = {
|
||||||
filters: TaskMetaFilters;
|
filters: TaskMetaFilters;
|
||||||
userID: string;
|
userID: string;
|
||||||
projectID: string;
|
labels: React.RefObject<Array<ProjectLabel>>;
|
||||||
members: React.RefObject<Array<TaskUser>>;
|
members: React.RefObject<Array<TaskUser>>;
|
||||||
onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void;
|
onChangeTaskMetaFilter: (filters: TaskMetaFilters) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ControlFilter: React.FC<ControlFilterProps> = ({
|
const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter, userID, labels, members }) => {
|
||||||
filters,
|
|
||||||
onChangeTaskMetaFilter,
|
|
||||||
userID,
|
|
||||||
projectID,
|
|
||||||
members,
|
|
||||||
}) => {
|
|
||||||
const [currentFilters, setFilters] = useState(filters);
|
const [currentFilters, setFilters] = useState(filters);
|
||||||
const [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : '');
|
const [nameFilter, setNameFilter] = useState(filters.taskName ? filters.taskName.name : '');
|
||||||
const [currentLabel, setCurrentLabel] = useState('');
|
const [currentLabel, setCurrentLabel] = useState('');
|
||||||
const { data } = useLabelsQuery({ variables: { projectID } });
|
|
||||||
|
|
||||||
const handleSetFilters = (f: TaskMetaFilters) => {
|
const handleSetFilters = (f: TaskMetaFilters) => {
|
||||||
setFilters(f);
|
setFilters(f);
|
||||||
@ -135,7 +127,7 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
|
|
||||||
const handleNameChange = (nFilter: string) => {
|
const handleNameChange = (nFilter: string) => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, (draftFilters) => {
|
produce(currentFilters, draftFilters => {
|
||||||
draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null;
|
draftFilters.taskName = nFilter !== '' ? { name: nFilter } : null;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -146,7 +138,7 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
|
|
||||||
const handleSetDueDate = (filterType: DueDateFilterType, label: string) => {
|
const handleSetDueDate = (filterType: DueDateFilterType, label: string) => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, (draftFilters) => {
|
produce(currentFilters, draftFilters => {
|
||||||
if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) {
|
if (draftFilters.dueDate && draftFilters.dueDate.type === filterType) {
|
||||||
draftFilters.dueDate = null;
|
draftFilters.dueDate = null;
|
||||||
} else {
|
} else {
|
||||||
@ -165,7 +157,7 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
<ActionsList>
|
<ActionsList>
|
||||||
<TaskNameInput
|
<TaskNameInput
|
||||||
width="100%"
|
width="100%"
|
||||||
onChange={(e) => handleNameChange(e.currentTarget.value)}
|
onChange={e => handleNameChange(e.currentTarget.value)}
|
||||||
value={nameFilter}
|
value={nameFilter}
|
||||||
autoFocus
|
autoFocus
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
@ -175,14 +167,14 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
<ActionItem
|
<ActionItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, (draftFilters) => {
|
produce(currentFilters, draftFilters => {
|
||||||
if (members.current) {
|
if (members.current) {
|
||||||
const member = members.current.find((m) => m.id === userID);
|
const member = members.current.find(m => m.id === userID);
|
||||||
const draftMember = draftFilters.members.find((m) => m.id === userID);
|
const draftMember = draftFilters.members.find(m => m.id === userID);
|
||||||
if (member && !draftMember) {
|
if (member && !draftMember) {
|
||||||
draftFilters.members.push({ id: userID, username: member.username ? member.username : '' });
|
draftFilters.members.push({ id: userID, username: member.username ? member.username : '' });
|
||||||
} else {
|
} else {
|
||||||
draftFilters.members = draftFilters.members.filter((m) => m.id !== userID);
|
draftFilters.members = draftFilters.members.filter(m => m.id !== userID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -193,7 +185,7 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
<User width={12} height={12} />
|
<User width={12} height={12} />
|
||||||
</ItemIcon>
|
</ItemIcon>
|
||||||
<ActionTitle>Just my tasks</ActionTitle>
|
<ActionTitle>Just my tasks</ActionTitle>
|
||||||
{currentFilters.members.find((m) => m.id === userID) && <ActiveIcon width={12} height={12} />}
|
{currentFilters.members.find(m => m.id === userID) && <ActiveIcon width={12} height={12} />}
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<ActionItem onClick={() => handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}>
|
<ActionItem onClick={() => handleSetDueDate(DueDateFilterType.THIS_WEEK, 'Due this week')}>
|
||||||
<ItemIcon>
|
<ItemIcon>
|
||||||
@ -236,10 +228,10 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
</Popup>
|
</Popup>
|
||||||
<Popup tab={1} title="By Labels">
|
<Popup tab={1} title="By Labels">
|
||||||
<Labels>
|
<Labels>
|
||||||
{data &&
|
{labels.current &&
|
||||||
data.findProject.labels
|
labels.current
|
||||||
// .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase())))
|
// .filter(label => '' === '' || (label.name && label.name.toLowerCase().startsWith(''.toLowerCase())))
|
||||||
.map((label) => (
|
.map(label => (
|
||||||
<Label key={label.id}>
|
<Label key={label.id}>
|
||||||
<CardLabel
|
<CardLabel
|
||||||
key={label.id}
|
key={label.id}
|
||||||
@ -250,9 +242,9 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, (draftFilters) => {
|
produce(currentFilters, draftFilters => {
|
||||||
if (draftFilters.labels.find((l) => l.id === label.id)) {
|
if (draftFilters.labels.find(l => l.id === label.id)) {
|
||||||
draftFilters.labels = draftFilters.labels.filter((l) => l.id !== label.id);
|
draftFilters.labels = draftFilters.labels.filter(l => l.id !== label.id);
|
||||||
} else {
|
} else {
|
||||||
draftFilters.labels.push({
|
draftFilters.labels.push({
|
||||||
id: label.id,
|
id: label.id,
|
||||||
@ -273,16 +265,16 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
<Popup tab={2} title="By Member">
|
<Popup tab={2} title="By Member">
|
||||||
<ActionsList>
|
<ActionsList>
|
||||||
{members.current &&
|
{members.current &&
|
||||||
members.current.map((member) => (
|
members.current.map(member => (
|
||||||
<FilterMember
|
<FilterMember
|
||||||
key={member.id}
|
key={member.id}
|
||||||
member={member}
|
member={member}
|
||||||
showName
|
showName
|
||||||
onCardMemberClick={() => {
|
onCardMemberClick={() => {
|
||||||
handleSetFilters(
|
handleSetFilters(
|
||||||
produce(currentFilters, (draftFilters) => {
|
produce(currentFilters, draftFilters => {
|
||||||
if (draftFilters.members.find((m) => m.id === member.id)) {
|
if (draftFilters.members.find(m => m.id === member.id)) {
|
||||||
draftFilters.members = draftFilters.members.filter((m) => m.id !== member.id);
|
draftFilters.members = draftFilters.members.filter(m => m.id !== member.id);
|
||||||
} else {
|
} else {
|
||||||
draftFilters.members.push({ id: member.id, username: member.username ?? '' });
|
draftFilters.members.push({ id: member.id, username: member.username ?? '' });
|
||||||
}
|
}
|
||||||
@ -329,4 +321,4 @@ const ControlFilter: React.FC<ControlFilterProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ControlFilter;
|
export default FilterMeta;
|
@ -30,7 +30,7 @@ export const ActionItem = styled.li`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
&:hover ${ActionExtraMenuContainer} {
|
&:hover ${ActionExtraMenuContainer} {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
@ -69,11 +69,11 @@ export const ActionExtraMenuItem = styled.li`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const ActionExtraMenuSeparator = styled.li`
|
const ActionExtraMenuSeparator = styled.li`
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
@ -85,12 +85,12 @@ const ActiveIcon = styled(Checkmark)`
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ControlStatusProps = {
|
type FilterStatusProps = {
|
||||||
filter: TaskStatusFilter;
|
filter: TaskStatusFilter;
|
||||||
onChangeTaskStatusFilter: (filter: TaskStatusFilter) => void;
|
onChangeTaskStatusFilter: (filter: TaskStatusFilter) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ControlStatus: React.FC<ControlStatusProps> = ({ filter, onChangeTaskStatusFilter }) => {
|
const FilterStatus: React.FC<FilterStatusProps> = ({ filter, onChangeTaskStatusFilter }) => {
|
||||||
const [currentFilter, setFilter] = useState(filter);
|
const [currentFilter, setFilter] = useState(filter);
|
||||||
const handleFilterChange = (f: TaskStatusFilter) => {
|
const handleFilterChange = (f: TaskStatusFilter) => {
|
||||||
setFilter(f);
|
setFilter(f);
|
||||||
@ -146,4 +146,4 @@ const ControlStatus: React.FC<ControlStatusProps> = ({ filter, onChangeTaskStatu
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ControlStatus;
|
export default FilterStatus;
|
@ -1,11 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
|
import { TaskSorting, TaskSortingType, TaskSortingDirection } from 'shared/utils/sorting';
|
||||||
import { Checkmark } from 'shared/icons';
|
import { mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
const ActiveIcon = styled(Checkmark)`
|
|
||||||
position: absolute;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ActionsList = styled.ul`
|
export const ActionsList = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -25,7 +21,7 @@ export const ActionItem = styled.li`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -33,12 +29,21 @@ export const ActionTitle = styled.span`
|
|||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ControlSortProps = {
|
const ActionItemSeparator = styled.li`
|
||||||
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
||||||
|
font-size: 12px;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type SortPopupProps = {
|
||||||
sorting: TaskSorting;
|
sorting: TaskSorting;
|
||||||
onChangeTaskSorting: (taskSorting: TaskSorting) => void;
|
onChangeTaskSorting: (taskSorting: TaskSorting) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ControlSort: React.FC<ControlSortProps> = ({ sorting, onChangeTaskSorting }) => {
|
const SortPopup: React.FC<SortPopupProps> = ({ sorting, onChangeTaskSorting }) => {
|
||||||
const [currentSorting, setSorting] = useState(sorting);
|
const [currentSorting, setSorting] = useState(sorting);
|
||||||
const handleSetSorting = (s: TaskSorting) => {
|
const handleSetSorting = (s: TaskSorting) => {
|
||||||
setSorting(s);
|
setSorting(s);
|
||||||
@ -47,41 +52,35 @@ const ControlSort: React.FC<ControlSortProps> = ({ sorting, onChangeTaskSorting
|
|||||||
return (
|
return (
|
||||||
<ActionsList>
|
<ActionsList>
|
||||||
<ActionItem onClick={() => handleSetSorting({ type: TaskSortingType.NONE, direction: TaskSortingDirection.ASC })}>
|
<ActionItem onClick={() => handleSetSorting({ type: TaskSortingType.NONE, direction: TaskSortingDirection.ASC })}>
|
||||||
{currentSorting.type === TaskSortingType.NONE && <ActiveIcon width={12} height={12} />}
|
|
||||||
<ActionTitle>None</ActionTitle>
|
<ActionTitle>None</ActionTitle>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<ActionItem
|
<ActionItem
|
||||||
onClick={() => handleSetSorting({ type: TaskSortingType.DUE_DATE, direction: TaskSortingDirection.ASC })}
|
onClick={() => handleSetSorting({ type: TaskSortingType.DUE_DATE, direction: TaskSortingDirection.ASC })}
|
||||||
>
|
>
|
||||||
{currentSorting.type === TaskSortingType.DUE_DATE && <ActiveIcon width={12} height={12} />}
|
|
||||||
<ActionTitle>Due date</ActionTitle>
|
<ActionTitle>Due date</ActionTitle>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<ActionItem
|
<ActionItem
|
||||||
onClick={() => handleSetSorting({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })}
|
onClick={() => handleSetSorting({ type: TaskSortingType.MEMBERS, direction: TaskSortingDirection.ASC })}
|
||||||
>
|
>
|
||||||
{currentSorting.type === TaskSortingType.MEMBERS && <ActiveIcon width={12} height={12} />}
|
|
||||||
<ActionTitle>Members</ActionTitle>
|
<ActionTitle>Members</ActionTitle>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<ActionItem
|
<ActionItem
|
||||||
onClick={() => handleSetSorting({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })}
|
onClick={() => handleSetSorting({ type: TaskSortingType.LABELS, direction: TaskSortingDirection.ASC })}
|
||||||
>
|
>
|
||||||
{currentSorting.type === TaskSortingType.LABELS && <ActiveIcon width={12} height={12} />}
|
|
||||||
<ActionTitle>Labels</ActionTitle>
|
<ActionTitle>Labels</ActionTitle>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<ActionItem
|
<ActionItem
|
||||||
onClick={() => handleSetSorting({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })}
|
onClick={() => handleSetSorting({ type: TaskSortingType.TASK_TITLE, direction: TaskSortingDirection.ASC })}
|
||||||
>
|
>
|
||||||
{currentSorting.type === TaskSortingType.TASK_TITLE && <ActiveIcon width={12} height={12} />}
|
|
||||||
<ActionTitle>Task title</ActionTitle>
|
<ActionTitle>Task title</ActionTitle>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
<ActionItem
|
<ActionItem
|
||||||
onClick={() => handleSetSorting({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })}
|
onClick={() => handleSetSorting({ type: TaskSortingType.COMPLETE, direction: TaskSortingDirection.ASC })}
|
||||||
>
|
>
|
||||||
{currentSorting.type === TaskSortingType.COMPLETE && <ActiveIcon width={12} height={12} />}
|
|
||||||
<ActionTitle>Complete</ActionTitle>
|
<ActionTitle>Complete</ActionTitle>
|
||||||
</ActionItem>
|
</ActionItem>
|
||||||
</ActionsList>
|
</ActionsList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ControlSort;
|
export default SortPopup;
|
@ -49,9 +49,9 @@ import LabelManagerEditor from 'Projects/Project/LabelManagerEditor';
|
|||||||
import Chip from 'shared/components/Chip';
|
import Chip from 'shared/components/Chip';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser } from 'App/context';
|
||||||
import ControlStatus from './ControlStatus';
|
import FilterStatus from './FilterStatus';
|
||||||
import ControlFilter from './ControlFilter';
|
import FilterMeta from './FilterMeta';
|
||||||
import ControlSort from './ControlSort';
|
import SortPopup from './SortPopup';
|
||||||
|
|
||||||
const FilterChip = styled(Chip)`
|
const FilterChip = styled(Chip)`
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
@ -60,20 +60,19 @@ const FilterChip = styled(Chip)`
|
|||||||
type MetaFilterCloseFn = (meta: TaskMeta, key: string) => void;
|
type MetaFilterCloseFn = (meta: TaskMeta, key: string) => void;
|
||||||
|
|
||||||
const renderTaskSortingLabel = (sorting: TaskSorting) => {
|
const renderTaskSortingLabel = (sorting: TaskSorting) => {
|
||||||
switch (sorting.type) {
|
if (sorting.type === TaskSortingType.TASK_TITLE) {
|
||||||
case TaskSortingType.TASK_TITLE:
|
return 'Sort: Card title';
|
||||||
return 'Sort: Task Title';
|
|
||||||
case TaskSortingType.MEMBERS:
|
|
||||||
return 'Sort: Members';
|
|
||||||
case TaskSortingType.DUE_DATE:
|
|
||||||
return 'Sort: Due Date';
|
|
||||||
case TaskSortingType.LABELS:
|
|
||||||
return 'Sort: Labels';
|
|
||||||
case TaskSortingType.COMPLETE:
|
|
||||||
return 'Sort: Complete';
|
|
||||||
default:
|
|
||||||
return 'Sort';
|
|
||||||
}
|
}
|
||||||
|
if (sorting.type === TaskSortingType.MEMBERS) {
|
||||||
|
return 'Sort: Members';
|
||||||
|
}
|
||||||
|
if (sorting.type === TaskSortingType.DUE_DATE) {
|
||||||
|
return 'Sort: Due Date';
|
||||||
|
}
|
||||||
|
if (sorting.type === TaskSortingType.LABELS) {
|
||||||
|
return 'Sort: Labels';
|
||||||
|
}
|
||||||
|
return 'Sort';
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMetaFilters = (filters: TaskMetaFilters, onClose: MetaFilterCloseFn) => {
|
const renderMetaFilters = (filters: TaskMetaFilters, onClose: MetaFilterCloseFn) => {
|
||||||
@ -137,16 +136,16 @@ const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
${(props) =>
|
${props =>
|
||||||
props.disabled &&
|
props.disabled &&
|
||||||
css`
|
css`
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -199,7 +198,6 @@ type ProjectBoardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BoardLoading = () => {
|
export const BoardLoading = () => {
|
||||||
const { user } = useCurrentUser();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProjectBar>
|
<ProjectBar>
|
||||||
@ -217,7 +215,6 @@ export const BoardLoading = () => {
|
|||||||
<ProjectActionText>Filter</ProjectActionText>
|
<ProjectActionText>Filter</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
</ProjectActions>
|
</ProjectActions>
|
||||||
{user && (
|
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectAction>
|
<ProjectAction>
|
||||||
<Tags width={13} height={13} />
|
<Tags width={13} height={13} />
|
||||||
@ -232,7 +229,6 @@ export const BoardLoading = () => {
|
|||||||
<ProjectActionText>Rules</ProjectActionText>
|
<ProjectActionText>Rules</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
</ProjectActions>
|
</ProjectActions>
|
||||||
)}
|
|
||||||
</ProjectBar>
|
</ProjectBar>
|
||||||
<EmptyBoard />
|
<EmptyBoard />
|
||||||
</>
|
</>
|
||||||
@ -281,8 +277,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
draftCache.findProject.taskGroups = draftCache.findProject.taskGroups.filter(
|
||||||
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
|
(taskGroup: TaskGroup) => taskGroup.id !== deletedTaskGroupData.data?.deleteTaskGroup.taskGroup.id,
|
||||||
);
|
);
|
||||||
@ -297,10 +293,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
const { taskGroups } = cache.findProject;
|
const { taskGroups } = cache.findProject;
|
||||||
const idx = taskGroups.findIndex((taskGroup) => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
|
const idx = taskGroups.findIndex(taskGroup => taskGroup.id === newTaskData.data?.createTask.taskGroup.id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
if (newTaskData.data) {
|
if (newTaskData.data) {
|
||||||
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
|
||||||
@ -317,8 +313,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (newTaskGroupData.data) {
|
if (newTaskGroupData.data) {
|
||||||
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
|
||||||
}
|
}
|
||||||
@ -337,10 +333,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
const idx = cache.findProject.taskGroups.findIndex(
|
const idx = cache.findProject.taskGroups.findIndex(
|
||||||
(t) => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
|
t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
|
||||||
);
|
);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
draftCache.findProject.taskGroups[idx].tasks = [];
|
draftCache.findProject.taskGroups[idx].tasks = [];
|
||||||
@ -354,8 +350,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (resp.data) {
|
if (resp.data) {
|
||||||
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
|
draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
|
||||||
}
|
}
|
||||||
@ -372,8 +368,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (newTask.data) {
|
if (newTask.data) {
|
||||||
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
|
||||||
if (previousTaskGroupID !== task.taskGroup.id) {
|
if (previousTaskGroupID !== task.taskGroup.id) {
|
||||||
@ -381,9 +377,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
|
||||||
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
|
||||||
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
|
||||||
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(
|
const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
|
||||||
(t) => t.id === task.id,
|
|
||||||
);
|
|
||||||
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
|
||||||
(t: Task) => t.id !== task.id,
|
(t: Task) => t.id !== task.id,
|
||||||
);
|
);
|
||||||
@ -404,14 +398,14 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [deleteTask] = useDeleteTaskMutation();
|
const [deleteTask] = useDeleteTaskMutation();
|
||||||
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
||||||
onCompleted: (newTaskLabel) => {
|
onCompleted: newTaskLabel => {
|
||||||
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onCreateTask = (taskGroupID: string, name: string) => {
|
const onCreateTask = (taskGroupID: string, name: string) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
|
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
||||||
if (taskGroup) {
|
if (taskGroup) {
|
||||||
let position = 65535;
|
let position = 65535;
|
||||||
if (taskGroup.tasks.length !== 0) {
|
if (taskGroup.tasks.length !== 0) {
|
||||||
@ -429,9 +423,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
createTask: {
|
createTask: {
|
||||||
__typename: 'Task',
|
__typename: 'Task',
|
||||||
id: `${Math.round(Math.random() * -1000000)}`,
|
id: `${Math.round(Math.random() * -1000000)}`,
|
||||||
shortId: '',
|
|
||||||
name,
|
name,
|
||||||
watched: false,
|
|
||||||
complete: false,
|
complete: false,
|
||||||
completedAt: null,
|
completedAt: null,
|
||||||
hasTime: false,
|
hasTime: false,
|
||||||
@ -446,7 +438,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
checklist: null,
|
checklist: null,
|
||||||
},
|
},
|
||||||
position,
|
position,
|
||||||
dueDate: { at: null },
|
dueDate: null,
|
||||||
description: null,
|
description: null,
|
||||||
labels: [],
|
labels: [],
|
||||||
assigned: [],
|
assigned: [],
|
||||||
@ -477,13 +469,12 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
}
|
}
|
||||||
return 'All Tasks';
|
return 'All Tasks';
|
||||||
};
|
};
|
||||||
|
if (data && user) {
|
||||||
if (data) {
|
|
||||||
labelsRef.current = data.findProject.labels;
|
labelsRef.current = data.findProject.labels;
|
||||||
membersRef.current = data.findProject.members;
|
membersRef.current = data.findProject.members;
|
||||||
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
|
const onQuickEditorOpen = ($target: React.RefObject<HTMLElement>, taskID: string, taskGroupID: string) => {
|
||||||
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
|
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
||||||
const currentTask = taskGroup ? taskGroup.tasks.find((t) => t.id === taskID) : null;
|
const currentTask = taskGroup ? taskGroup.tasks.find(t => t.id === taskID) : null;
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
setQuickCardEditor({
|
setQuickCardEditor({
|
||||||
target: $target,
|
target: $target,
|
||||||
@ -495,9 +486,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
};
|
};
|
||||||
let currentQuickTask = null;
|
let currentQuickTask = null;
|
||||||
if (quickCardEditor.taskID && quickCardEditor.taskGroupID) {
|
if (quickCardEditor.taskID && quickCardEditor.taskGroupID) {
|
||||||
const targetGroup = data.findProject.taskGroups.find((t) => t.id === quickCardEditor.taskGroupID);
|
const targetGroup = data.findProject.taskGroups.find(t => t.id === quickCardEditor.taskGroupID);
|
||||||
if (targetGroup) {
|
if (targetGroup) {
|
||||||
currentQuickTask = targetGroup.tasks.find((t) => t.id === quickCardEditor.taskID);
|
currentQuickTask = targetGroup.tasks.find(t => t.id === quickCardEditor.taskID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -505,13 +496,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<ProjectBar>
|
<ProjectBar>
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={(target) => {
|
onClick={target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
target,
|
target,
|
||||||
<Popup tab={0} title={null}>
|
<Popup tab={0} title={null}>
|
||||||
<ControlStatus
|
<FilterStatus
|
||||||
filter={taskStatusFilter}
|
filter={taskStatusFilter}
|
||||||
onChangeTaskStatusFilter={(filter) => {
|
onChangeTaskStatusFilter={filter => {
|
||||||
setTaskStatusFilter(filter);
|
setTaskStatusFilter(filter);
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
@ -525,13 +516,13 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<ProjectActionText>{getTaskStatusFilterLabel(taskStatusFilter)}</ProjectActionText>
|
<ProjectActionText>{getTaskStatusFilterLabel(taskStatusFilter)}</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={(target) => {
|
onClick={target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
target,
|
target,
|
||||||
<Popup tab={0} title={null}>
|
<Popup tab={0} title={null}>
|
||||||
<ControlSort
|
<SortPopup
|
||||||
sorting={taskSorting}
|
sorting={taskSorting}
|
||||||
onChangeTaskSorting={(sorting) => {
|
onChangeTaskSorting={sorting => {
|
||||||
setTaskSorting(sorting);
|
setTaskSorting(sorting);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -544,16 +535,16 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<ProjectActionText>{renderTaskSortingLabel(taskSorting)}</ProjectActionText>
|
<ProjectActionText>{renderTaskSortingLabel(taskSorting)}</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={(target) => {
|
onClick={target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
target,
|
target,
|
||||||
<ControlFilter
|
<FilterMeta
|
||||||
filters={taskMetaFilters}
|
filters={taskMetaFilters}
|
||||||
onChangeTaskMetaFilter={(filter) => {
|
onChangeTaskMetaFilter={filter => {
|
||||||
setTaskMetaFilters(filter);
|
setTaskMetaFilters(filter);
|
||||||
}}
|
}}
|
||||||
userID={user ?? ''}
|
userID={user?.id}
|
||||||
projectID={projectID}
|
labels={labelsRef}
|
||||||
members={membersRef}
|
members={membersRef}
|
||||||
/>,
|
/>,
|
||||||
{ width: 200 },
|
{ width: 200 },
|
||||||
@ -565,11 +556,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
{renderMetaFilters(taskMetaFilters, (meta, id) => {
|
{renderMetaFilters(taskMetaFilters, (meta, id) => {
|
||||||
setTaskMetaFilters(
|
setTaskMetaFilters(
|
||||||
produce(taskMetaFilters, (draftFilters) => {
|
produce(taskMetaFilters, draftFilters => {
|
||||||
if (meta === TaskMeta.MEMBER) {
|
if (meta === TaskMeta.MEMBER) {
|
||||||
draftFilters.members = draftFilters.members.filter((m) => m.id !== id);
|
draftFilters.members = draftFilters.members.filter(m => m.id !== id);
|
||||||
} else if (meta === TaskMeta.LABEL) {
|
} else if (meta === TaskMeta.LABEL) {
|
||||||
draftFilters.labels = draftFilters.labels.filter((m) => m.id !== id);
|
draftFilters.labels = draftFilters.labels.filter(m => m.id !== id);
|
||||||
} else if (meta === TaskMeta.TITLE) {
|
} else if (meta === TaskMeta.TITLE) {
|
||||||
draftFilters.taskName = null;
|
draftFilters.taskName = null;
|
||||||
} else if (meta === TaskMeta.DUE_DATE) {
|
} else if (meta === TaskMeta.DUE_DATE) {
|
||||||
@ -579,13 +570,17 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ProjectActions>
|
</ProjectActions>
|
||||||
{user && (
|
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectAction
|
<ProjectAction
|
||||||
onClick={($labelsRef) => {
|
onClick={$labelsRef => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$labelsRef,
|
$labelsRef,
|
||||||
<LabelManagerEditor taskLabels={null} labelColors={data.labelColors} projectID={projectID ?? ''} />,
|
<LabelManagerEditor
|
||||||
|
taskLabels={null}
|
||||||
|
labelColors={data.labelColors}
|
||||||
|
labels={labelsRef}
|
||||||
|
projectID={projectID ?? ''}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -601,12 +596,10 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<ProjectActionText>Rules</ProjectActionText>
|
<ProjectActionText>Rules</ProjectActionText>
|
||||||
</ProjectAction>
|
</ProjectAction>
|
||||||
</ProjectActions>
|
</ProjectActions>
|
||||||
)}
|
|
||||||
</ProjectBar>
|
</ProjectBar>
|
||||||
<SimpleLists
|
<SimpleLists
|
||||||
isPublic={user === null}
|
onTaskClick={task => {
|
||||||
onTaskClick={(task) => {
|
history.push(`${match.url}/c/${task.id}`);
|
||||||
history.push(`${match.url}/c/${task.shortId}`);
|
|
||||||
}}
|
}}
|
||||||
onCardLabelClick={onCardLabelClick ?? NOOP}
|
onCardLabelClick={onCardLabelClick ?? NOOP}
|
||||||
cardLabelVariant={cardLabelVariant ?? 'large'}
|
cardLabelVariant={cardLabelVariant ?? 'large'}
|
||||||
@ -638,7 +631,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onTaskGroupDrop={(droppedTaskGroup) => {
|
onTaskGroupDrop={droppedTaskGroup => {
|
||||||
updateTaskGroupLocation({
|
updateTaskGroupLocation({
|
||||||
variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position },
|
variables: { taskGroupID: droppedTaskGroup.id, position: droppedTaskGroup.position },
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
@ -658,7 +651,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
onCreateTask={onCreateTask}
|
onCreateTask={onCreateTask}
|
||||||
onCreateTaskGroup={onCreateList}
|
onCreateTaskGroup={onCreateList}
|
||||||
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
||||||
const member = data.findProject.members.find((m) => m.id === memberID);
|
const member = data.findProject.members.find(m => m.id === memberID);
|
||||||
if (member) {
|
if (member) {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
@ -685,8 +678,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
deleteTaskGroupTasks({ variables: { taskGroupID } });
|
deleteTaskGroupTasks({ variables: { taskGroupID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onSortTaskGroup={(taskSort) => {
|
onSortTaskGroup={taskSort => {
|
||||||
const taskGroup = data.findProject.taskGroups.find((t) => t.id === taskGroupID);
|
const taskGroup = data.findProject.taskGroups.find(t => t.id === taskGroupID);
|
||||||
if (taskGroup) {
|
if (taskGroup) {
|
||||||
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
|
const tasks: Array<{ taskID: string; position: number }> = taskGroup.tasks
|
||||||
.sort((a, b) => sortTasks(a, b, taskSort))
|
.sort((a, b) => sortTasks(a, b, taskSort))
|
||||||
@ -698,8 +691,8 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDuplicateTaskGroup={(newName) => {
|
onDuplicateTaskGroup={newName => {
|
||||||
const idx = data.findProject.taskGroups.findIndex((t) => t.id === taskGroupID);
|
const idx = data.findProject.taskGroups.findIndex(t => t.id === taskGroupID);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
|
const taskGroups = data.findProject.taskGroups.sort((a, b) => a.position - b.position);
|
||||||
const prevPos = taskGroups[idx].position;
|
const prevPos = taskGroups[idx].position;
|
||||||
@ -712,7 +705,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onArchiveTaskGroup={(tgID) => {
|
onArchiveTaskGroup={tgID => {
|
||||||
deleteTaskGroup({ variables: { taskGroupID: tgID } });
|
deleteTaskGroup({ variables: { taskGroupID: tgID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
@ -746,7 +739,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
onCardMemberClick={($targetRef, _taskID, memberID) => {
|
||||||
const member = data.findProject.members.find((m) => m.id === memberID);
|
const member = data.findProject.members.find(m => m.id === memberID);
|
||||||
if (member) {
|
if (member) {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
@ -765,11 +758,11 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
<LabelManagerEditor
|
<LabelManagerEditor
|
||||||
onLabelToggle={(labelID) => {
|
onLabelToggle={labelID => {
|
||||||
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
|
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
|
||||||
}}
|
}}
|
||||||
taskID={task.id}
|
|
||||||
labelColors={data.labelColors}
|
labelColors={data.labelColors}
|
||||||
|
labels={labelsRef}
|
||||||
taskLabels={taskLabelsRef}
|
taskLabels={taskLabelsRef}
|
||||||
projectID={projectID ?? ''}
|
projectID={projectID ?? ''}
|
||||||
/>,
|
/>,
|
||||||
@ -778,15 +771,15 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
onArchiveCard={(_listId: string, cardId: string) => {
|
onArchiveCard={(_listId: string, cardId: string) => {
|
||||||
return deleteTask({
|
return deleteTask({
|
||||||
variables: { taskID: cardId },
|
variables: { taskID: cardId },
|
||||||
update: (client) => {
|
update: client => {
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map((taskGroup) => ({
|
draftCache.findProject.taskGroups = cache.findProject.taskGroups.map(taskGroup => ({
|
||||||
...taskGroup,
|
...taskGroup,
|
||||||
tasks: taskGroup.tasks.filter((t) => t.id !== cardId),
|
tasks: taskGroup.tasks.filter(t => t.id !== cardId),
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
@ -800,38 +793,20 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
|
|||||||
<Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}>
|
<Popup title="Change Due Date" tab={0} onClose={() => hidePopup()}>
|
||||||
<DueDateManager
|
<DueDateManager
|
||||||
task={task}
|
task={task}
|
||||||
onRemoveDueDate={(t) => {
|
onRemoveDueDate={t => {
|
||||||
hidePopup();
|
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
|
||||||
updateTaskDueDate({
|
// hidePopup();
|
||||||
variables: {
|
|
||||||
taskID: t.id,
|
|
||||||
dueDate: null,
|
|
||||||
hasTime: false,
|
|
||||||
deleteNotifications: [],
|
|
||||||
updateNotifications: [],
|
|
||||||
createNotifications: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onDueDateChange={(t, newDueDate, hasTime) => {
|
onDueDateChange={(t, newDueDate, hasTime) => {
|
||||||
hidePopup();
|
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
|
||||||
updateTaskDueDate({
|
// hidePopup();
|
||||||
variables: {
|
|
||||||
taskID: t.id,
|
|
||||||
dueDate: newDueDate,
|
|
||||||
hasTime,
|
|
||||||
deleteNotifications: [],
|
|
||||||
updateNotifications: [],
|
|
||||||
createNotifications: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onCancel={NOOP}
|
onCancel={NOOP}
|
||||||
/>
|
/>
|
||||||
</Popup>,
|
</Popup>,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onToggleComplete={(task) => {
|
onToggleComplete={task => {
|
||||||
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
|
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
|
||||||
}}
|
}}
|
||||||
target={quickCardEditor.target}
|
target={quickCardEditor.target}
|
||||||
|
@ -4,15 +4,13 @@ import TaskDetails from 'shared/components/TaskDetails';
|
|||||||
import TaskDetailsLoading from 'shared/components/TaskDetails/Loading';
|
import TaskDetailsLoading from 'shared/components/TaskDetails/Loading';
|
||||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
|
import { Popup, usePopup } from 'shared/components/PopupMenu';
|
||||||
import MemberManager from 'shared/components/MemberManager';
|
import MemberManager from 'shared/components/MemberManager';
|
||||||
import { useRouteMatch, useHistory, useParams } from 'react-router';
|
import { useRouteMatch, useHistory } from 'react-router';
|
||||||
import {
|
import {
|
||||||
useDeleteTaskChecklistMutation,
|
useDeleteTaskChecklistMutation,
|
||||||
useToggleTaskWatchMutation,
|
|
||||||
useUpdateTaskChecklistNameMutation,
|
useUpdateTaskChecklistNameMutation,
|
||||||
useUpdateTaskChecklistItemLocationMutation,
|
useUpdateTaskChecklistItemLocationMutation,
|
||||||
useCreateTaskChecklistMutation,
|
useCreateTaskChecklistMutation,
|
||||||
useFindTaskQuery,
|
useFindTaskQuery,
|
||||||
DueDateNotificationDuration,
|
|
||||||
useUpdateTaskDueDateMutation,
|
useUpdateTaskDueDateMutation,
|
||||||
useSetTaskCompleteMutation,
|
useSetTaskCompleteMutation,
|
||||||
useAssignTaskMutation,
|
useAssignTaskMutation,
|
||||||
@ -38,7 +36,6 @@ import Input from 'shared/components/Input';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import polling from 'shared/utils/polling';
|
|
||||||
|
|
||||||
export const ActionsList = styled.ul`
|
export const ActionsList = styled.ul`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -58,7 +55,7 @@ export const ActionItem = styled.li`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -168,8 +165,10 @@ const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChe
|
|||||||
defaultValue="Checklist"
|
defaultValue="Checklist"
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Name"
|
label="Name"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
variant="alternate"
|
variant="alternate"
|
||||||
{...register('name', { required: 'Checklist name is required' })}
|
ref={register({ required: 'Checklist name is required' })}
|
||||||
/>
|
/>
|
||||||
<CreateChecklistButton type="submit">Create</CreateChecklistButton>
|
<CreateChecklistButton type="submit">Create</CreateChecklistButton>
|
||||||
</CreateChecklistForm>
|
</CreateChecklistForm>
|
||||||
@ -177,6 +176,7 @@ const CreateChecklistPopup: React.FC<CreateChecklistPopupProps> = ({ onCreateChe
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DetailsProps = {
|
type DetailsProps = {
|
||||||
|
taskID: string;
|
||||||
projectURL: string;
|
projectURL: string;
|
||||||
onTaskNameChange: (task: Task, newName: string) => void;
|
onTaskNameChange: (task: Task, newName: string) => void;
|
||||||
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
|
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
|
||||||
@ -190,6 +190,7 @@ const initialMemberPopupState = { taskID: '', isOpen: false, top: 0, left: 0 };
|
|||||||
|
|
||||||
const Details: React.FC<DetailsProps> = ({
|
const Details: React.FC<DetailsProps> = ({
|
||||||
projectURL,
|
projectURL,
|
||||||
|
taskID,
|
||||||
onTaskNameChange,
|
onTaskNameChange,
|
||||||
onTaskDescriptionChange,
|
onTaskDescriptionChange,
|
||||||
onDeleteTask,
|
onDeleteTask,
|
||||||
@ -198,7 +199,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
refreshCache,
|
refreshCache,
|
||||||
}) => {
|
}) => {
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const { taskID } = useParams<{ taskID: string }>();
|
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [deleteTaskComment] = useDeleteTaskCommentMutation({
|
const [deleteTaskComment] = useDeleteTaskCommentMutation({
|
||||||
@ -206,11 +206,11 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
draftCache.findTask.comments = cache.findTask.comments.filter(
|
draftCache.findTask.comments = cache.findTask.comments.filter(
|
||||||
(c) => c.id !== response.data?.deleteTaskComment.commentID,
|
c => c.id !== response.data?.deleteTaskComment.commentID,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -218,14 +218,13 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [toggleTaskWatch] = useToggleTaskWatchMutation();
|
|
||||||
const [createTaskComment] = useCreateTaskCommentMutation({
|
const [createTaskComment] = useCreateTaskCommentMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
draftCache.findTask.comments.push({
|
draftCache.findTask.comments.push({
|
||||||
...response.data.createTaskComment.comment,
|
...response.data.createTaskComment.comment,
|
||||||
@ -242,18 +241,18 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
|
const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
|
||||||
if (taskChecklistID !== prevChecklistID) {
|
if (taskChecklistID !== prevChecklistID) {
|
||||||
const oldIdx = cache.findTask.checklists.findIndex((c) => c.id === prevChecklistID);
|
const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
|
||||||
const newIdx = cache.findTask.checklists.findIndex((c) => c.id === taskChecklistID);
|
const newIdx = cache.findTask.checklists.findIndex(c => c.id === taskChecklistID);
|
||||||
if (oldIdx > -1 && newIdx > -1) {
|
if (oldIdx > -1 && newIdx > -1) {
|
||||||
const item = cache.findTask.checklists[oldIdx].items.find((i) => i.id === checklistItem.id);
|
const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
|
||||||
if (item) {
|
if (item) {
|
||||||
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
|
draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
|
||||||
(i) => i.id !== checklistItem.id,
|
i => i.id !== checklistItem.id,
|
||||||
);
|
);
|
||||||
draftCache.findTask.checklists[newIdx].items.push({
|
draftCache.findTask.checklists[newIdx].items.push({
|
||||||
...item,
|
...item,
|
||||||
@ -270,12 +269,12 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({
|
const [setTaskChecklistItemComplete] = useSetTaskChecklistItemCompleteMutation({
|
||||||
update: (client) => {
|
update: client => {
|
||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
||||||
draftCache.findTask.badges.checklist = {
|
draftCache.findTask.badges.checklist = {
|
||||||
__typename: 'ChecklistBadge',
|
__typename: 'ChecklistBadge',
|
||||||
@ -292,11 +291,11 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
const { checklists } = cache.findTask;
|
const { checklists } = cache.findTask;
|
||||||
draftCache.findTask.checklists = checklists.filter(
|
draftCache.findTask.checklists = checklists.filter(
|
||||||
(c) => c.id !== deleteData.data?.deleteTaskChecklist.taskChecklist.id,
|
c => c.id !== deleteData.data?.deleteTaskChecklist.taskChecklist.id,
|
||||||
);
|
);
|
||||||
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
||||||
draftCache.findTask.badges.checklist = {
|
draftCache.findTask.badges.checklist = {
|
||||||
@ -318,8 +317,8 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (createData.data) {
|
if (createData.data) {
|
||||||
const item = createData.data.createTaskChecklist;
|
const item = createData.data.createTaskChecklist;
|
||||||
draftCache.findTask.checklists.push({ ...item });
|
draftCache.findTask.checklists.push({ ...item });
|
||||||
@ -335,14 +334,14 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (deleteData.data) {
|
if (deleteData.data) {
|
||||||
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
|
const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
|
||||||
const targetIdx = cache.findTask.checklists.findIndex((c) => c.id === item.taskChecklistID);
|
const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
|
||||||
if (targetIdx > -1) {
|
if (targetIdx > -1) {
|
||||||
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
|
draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
|
||||||
(c) => item.id !== c.id,
|
c => item.id !== c.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
||||||
@ -362,12 +361,12 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
updateApolloCache<FindTaskQuery>(
|
updateApolloCache<FindTaskQuery>(
|
||||||
client,
|
client,
|
||||||
FindTaskDocument,
|
FindTaskDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (newTaskItem.data) {
|
if (newTaskItem.data) {
|
||||||
const item = newTaskItem.data.createTaskChecklistItem;
|
const item = newTaskItem.data.createTaskChecklistItem;
|
||||||
const { checklists } = cache.findTask;
|
const { checklists } = cache.findTask;
|
||||||
const idx = checklists.findIndex((c) => c.id === item.taskChecklistID);
|
const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
draftCache.findTask.checklists[idx].items.push({ ...item });
|
draftCache.findTask.checklists[idx].items.push({ ...item });
|
||||||
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
|
||||||
@ -385,7 +384,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
});
|
});
|
||||||
const { loading, data, refetch } = useFindTaskQuery({
|
const { loading, data, refetch } = useFindTaskQuery({
|
||||||
variables: { taskID },
|
variables: { taskID },
|
||||||
pollInterval: polling.TASK_DETAILS,
|
pollInterval: 3000,
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
});
|
});
|
||||||
const [setTaskComplete] = useSetTaskCompleteMutation();
|
const [setTaskComplete] = useSetTaskCompleteMutation();
|
||||||
@ -416,7 +415,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
width={1070}
|
width={1070}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
history.push(projectURL);
|
history.push(projectURL);
|
||||||
hidePopup();
|
|
||||||
}}
|
}}
|
||||||
renderContent={() => {
|
renderContent={() => {
|
||||||
return data ? (
|
return data ? (
|
||||||
@ -426,7 +424,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
updateTaskComment({ variables: { commentID, message } });
|
updateTaskComment({ variables: { commentID, message } });
|
||||||
}}
|
}}
|
||||||
editableComment={editableComment}
|
editableComment={editableComment}
|
||||||
me={data.me ? data.me.user : null}
|
me={data.me.user}
|
||||||
onCommentShowActions={(commentID, $targetRef) => {
|
onCommentShowActions={(commentID, $targetRef) => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
@ -443,23 +441,10 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
task={data.findTask}
|
task={data.findTask}
|
||||||
onToggleTaskWatch={(task, watched) => {
|
|
||||||
toggleTaskWatch({
|
|
||||||
variables: { taskID: task.id },
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: 'Mutation',
|
|
||||||
toggleTaskWatch: {
|
|
||||||
id: task.id,
|
|
||||||
__typename: 'Task',
|
|
||||||
watched,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onCreateComment={(task, message) => {
|
onCreateComment={(task, message) => {
|
||||||
createTaskComment({ variables: { taskID: task.id, message } });
|
createTaskComment({ variables: { taskID: task.id, message } });
|
||||||
}}
|
}}
|
||||||
onChecklistDrop={(checklist) => {
|
onChecklistDrop={checklist => {
|
||||||
updateTaskChecklistLocation({
|
updateTaskChecklistLocation({
|
||||||
variables: { taskChecklistID: checklist.id, position: checklist.position },
|
variables: { taskChecklistID: checklist.id, position: checklist.position },
|
||||||
|
|
||||||
@ -501,7 +486,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
}}
|
}}
|
||||||
onTaskNameChange={onTaskNameChange}
|
onTaskNameChange={onTaskNameChange}
|
||||||
onTaskDescriptionChange={onTaskDescriptionChange}
|
onTaskDescriptionChange={onTaskDescriptionChange}
|
||||||
onToggleTaskComplete={(task) => {
|
onToggleTaskComplete={task => {
|
||||||
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
|
setTaskComplete({ variables: { taskID: task.id, complete: !task.complete } });
|
||||||
}}
|
}}
|
||||||
onDeleteTask={onDeleteTask}
|
onDeleteTask={onDeleteTask}
|
||||||
@ -546,7 +531,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
createTaskChecklistItem({ variables: { taskChecklistID, name, position } });
|
createTaskChecklistItem({ variables: { taskChecklistID, name, position } });
|
||||||
}}
|
}}
|
||||||
onMemberProfile={($targetRef, memberID) => {
|
onMemberProfile={($targetRef, memberID) => {
|
||||||
const member = data.findTask.assigned.find((m) => m.id === memberID);
|
const member = data.findTask.assigned.find(m => m.id === memberID);
|
||||||
if (member) {
|
if (member) {
|
||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
@ -556,8 +541,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
bio="None"
|
bio="None"
|
||||||
onRemoveFromTask={() => {
|
onRemoveFromTask={() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
unassignTask({ variables: { taskID: data.findTask.id, userID: member.id ?? '' } });
|
unassignTask({ variables: { taskID: data.findTask.id, userID: user.id } });
|
||||||
hidePopup();
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -597,7 +581,7 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CreateChecklistPopup
|
<CreateChecklistPopup
|
||||||
onCreateChecklist={(checklistData) => {
|
onCreateChecklist={checklistData => {
|
||||||
let position = 65535;
|
let position = 65535;
|
||||||
if (data.findTask.checklists) {
|
if (data.findTask.checklists) {
|
||||||
const [lastChecklist] = data.findTask.checklists.slice(-1);
|
const [lastChecklist] = data.findTask.checklists.slice(-1);
|
||||||
@ -647,80 +631,13 @@ const Details: React.FC<DetailsProps> = ({
|
|||||||
>
|
>
|
||||||
<DueDateManager
|
<DueDateManager
|
||||||
task={task}
|
task={task}
|
||||||
onRemoveDueDate={(t) => {
|
onRemoveDueDate={t => {
|
||||||
updateTaskDueDate({
|
updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
|
||||||
variables: {
|
// hidePopup();
|
||||||
taskID: t.id,
|
|
||||||
dueDate: null,
|
|
||||||
hasTime: false,
|
|
||||||
deleteNotifications: t.dueDate.notifications
|
|
||||||
? t.dueDate.notifications.map((n) => ({ id: n.id }))
|
|
||||||
: [],
|
|
||||||
updateNotifications: [],
|
|
||||||
createNotifications: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
hidePopup();
|
|
||||||
}}
|
}}
|
||||||
onDueDateChange={(t, newDueDate, hasTime, notifications) => {
|
onDueDateChange={(t, newDueDate, hasTime) => {
|
||||||
const updatedNotifications = notifications.current
|
updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
|
||||||
.filter((c) => c.externalId !== null)
|
// hidePopup();
|
||||||
.map((c) => {
|
|
||||||
let duration = DueDateNotificationDuration.Minute;
|
|
||||||
switch (c.duration.value) {
|
|
||||||
case 'hour':
|
|
||||||
duration = DueDateNotificationDuration.Hour;
|
|
||||||
break;
|
|
||||||
case 'day':
|
|
||||||
duration = DueDateNotificationDuration.Day;
|
|
||||||
break;
|
|
||||||
case 'week':
|
|
||||||
duration = DueDateNotificationDuration.Week;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: c.externalId ?? '',
|
|
||||||
period: c.period,
|
|
||||||
duration,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const newNotifications = notifications.current
|
|
||||||
.filter((c) => c.externalId === null)
|
|
||||||
.map((c) => {
|
|
||||||
let duration = DueDateNotificationDuration.Minute;
|
|
||||||
switch (c.duration.value) {
|
|
||||||
case 'hour':
|
|
||||||
duration = DueDateNotificationDuration.Hour;
|
|
||||||
break;
|
|
||||||
case 'day':
|
|
||||||
duration = DueDateNotificationDuration.Day;
|
|
||||||
break;
|
|
||||||
case 'week':
|
|
||||||
duration = DueDateNotificationDuration.Week;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
taskID: task.id,
|
|
||||||
period: c.period,
|
|
||||||
duration,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
// const updatedNotifications = notifications.filter(c => c.externalId === null);
|
|
||||||
updateTaskDueDate({
|
|
||||||
variables: {
|
|
||||||
taskID: t.id,
|
|
||||||
dueDate: newDueDate,
|
|
||||||
hasTime,
|
|
||||||
createNotifications: newNotifications,
|
|
||||||
updateNotifications: updatedNotifications,
|
|
||||||
deleteNotifications: notifications.removed.map((n) => ({ id: n })),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
hidePopup();
|
|
||||||
}}
|
}}
|
||||||
onCancel={NOOP}
|
onCancel={NOOP}
|
||||||
/>
|
/>
|
||||||
|
@ -8,14 +8,12 @@ import {
|
|||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
useCreateProjectLabelMutation,
|
useCreateProjectLabelMutation,
|
||||||
FindProjectQuery,
|
FindProjectQuery,
|
||||||
useToggleTaskLabelMutation,
|
|
||||||
useLabelsQuery,
|
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import LabelManager from 'shared/components/PopupMenu/LabelManager';
|
import LabelManager from 'shared/components/PopupMenu/LabelManager';
|
||||||
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
import LabelEditor from 'shared/components/PopupMenu/LabelEditor';
|
||||||
|
|
||||||
type LabelManagerEditorProps = {
|
type LabelManagerEditorProps = {
|
||||||
taskID?: string;
|
labels: React.RefObject<Array<ProjectLabel>>;
|
||||||
taskLabels: null | React.RefObject<Array<TaskLabel>>;
|
taskLabels: null | React.RefObject<Array<TaskLabel>>;
|
||||||
projectID: string;
|
projectID: string;
|
||||||
labelColors: Array<LabelColor>;
|
labelColors: Array<LabelColor>;
|
||||||
@ -23,7 +21,7 @@ type LabelManagerEditorProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
||||||
taskID,
|
labels: labelsRef,
|
||||||
projectID,
|
projectID,
|
||||||
labelColors,
|
labelColors,
|
||||||
onLabelToggle,
|
onLabelToggle,
|
||||||
@ -31,19 +29,13 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [currentLabel, setCurrentLabel] = useState('');
|
const [currentLabel, setCurrentLabel] = useState('');
|
||||||
const { setTab, hidePopup } = usePopup();
|
const { setTab, hidePopup } = usePopup();
|
||||||
const [toggleTaskLabel] = useToggleTaskLabelMutation();
|
|
||||||
const [createProjectLabel] = useCreateProjectLabelMutation({
|
const [createProjectLabel] = useCreateProjectLabelMutation({
|
||||||
onCompleted: (data) => {
|
|
||||||
if (taskID) {
|
|
||||||
toggleTaskLabel({ variables: { taskID, projectLabelID: data.createProjectLabel.id } });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: (client, newLabelData) => {
|
update: (client, newLabelData) => {
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (newLabelData.data) {
|
if (newLabelData.data) {
|
||||||
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
|
||||||
}
|
}
|
||||||
@ -60,39 +52,38 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.labels = cache.findProject.labels.filter(
|
draftCache.findProject.labels = cache.findProject.labels.filter(
|
||||||
(label) => label.id !== newLabelData.data?.deleteProjectLabel.id,
|
label => label.id !== newLabelData.data?.deleteProjectLabel.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { data } = useLabelsQuery({ variables: { projectID } });
|
const labels = labelsRef.current ? labelsRef.current : [];
|
||||||
const labels = data ? data.findProject.labels : [];
|
|
||||||
const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : [];
|
const taskLabels = taskLabelsRef && taskLabelsRef.current ? taskLabelsRef.current : [];
|
||||||
const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
|
const [currentTaskLabels, setCurrentTaskLabels] = useState(taskLabels);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup title="Labels" tab={0} onClose={() => hidePopup()}>
|
<Popup title="Labels" tab={0} onClose={() => hidePopup()}>
|
||||||
<LabelManager
|
<LabelManager
|
||||||
labels={data ? data.findProject.labels : []}
|
labels={labels}
|
||||||
taskLabels={currentTaskLabels}
|
taskLabels={currentTaskLabels}
|
||||||
onLabelCreate={() => {
|
onLabelCreate={() => {
|
||||||
setTab(2);
|
setTab(2);
|
||||||
}}
|
}}
|
||||||
onLabelEdit={(labelId) => {
|
onLabelEdit={labelId => {
|
||||||
setCurrentLabel(labelId);
|
setCurrentLabel(labelId);
|
||||||
setTab(1);
|
setTab(1);
|
||||||
}}
|
}}
|
||||||
onLabelToggle={(labelId) => {
|
onLabelToggle={labelId => {
|
||||||
if (onLabelToggle) {
|
if (onLabelToggle) {
|
||||||
if (currentTaskLabels.find((t) => t.projectLabel.id === labelId)) {
|
if (currentTaskLabels.find(t => t.projectLabel.id === labelId)) {
|
||||||
setCurrentTaskLabels(currentTaskLabels.filter((t) => t.projectLabel.id !== labelId));
|
setCurrentTaskLabels(currentTaskLabels.filter(t => t.projectLabel.id !== labelId));
|
||||||
} else if (data) {
|
} else {
|
||||||
const newProjectLabel = data.findProject.labels.find((l) => l.id === labelId);
|
const newProjectLabel = labels.find(l => l.id === labelId);
|
||||||
if (newProjectLabel) {
|
if (newProjectLabel) {
|
||||||
setCurrentTaskLabels([
|
setCurrentTaskLabels([
|
||||||
...currentTaskLabels,
|
...currentTaskLabels,
|
||||||
@ -112,14 +103,14 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
|
|||||||
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
|
<Popup onClose={() => hidePopup()} title="Edit label" tab={1}>
|
||||||
<LabelEditor
|
<LabelEditor
|
||||||
labelColors={labelColors}
|
labelColors={labelColors}
|
||||||
label={labels.find((label) => label.id === currentLabel) ?? null}
|
label={labels.find(label => label.id === currentLabel) ?? null}
|
||||||
onLabelEdit={(projectLabelID, name, color) => {
|
onLabelEdit={(projectLabelID, name, color) => {
|
||||||
if (projectLabelID) {
|
if (projectLabelID) {
|
||||||
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
|
updateProjectLabel({ variables: { projectLabelID, labelColorID: color.id, name: name ?? '' } });
|
||||||
}
|
}
|
||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
onLabelDelete={(labelID) => {
|
onLabelDelete={labelID => {
|
||||||
deleteProjectLabel({ variables: { projectLabelID: labelID } });
|
deleteProjectLabel({ variables: { projectLabelID: labelID } });
|
||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Cross } from 'shared/icons';
|
|
||||||
import * as S from './Styles';
|
|
||||||
|
|
||||||
const OptionValue = ({ data, removeProps }: any) => {
|
|
||||||
return (
|
|
||||||
<S.OptionValueWrapper>
|
|
||||||
<S.OptionValueLabel>{data.label}</S.OptionValueLabel>
|
|
||||||
<S.OptionValueRemove {...removeProps}>
|
|
||||||
<Cross width={14} height={14} />
|
|
||||||
</S.OptionValueRemove>
|
|
||||||
</S.OptionValueWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OptionValue;
|
|
@ -1,64 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
import Button from 'shared/components/Button';
|
|
||||||
|
|
||||||
export const OptionWrapper = styled.div<{ isFocused: boolean }>`
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px 8px;
|
|
||||||
${props => props.isFocused && `background: rgba(${props.theme.colors.primary});`}
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const OptionContent = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-left: 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: ${p => p.fontSize}px;
|
|
||||||
color: rgba(${p => (p.quiet ? p.theme.colors.text.primary : p.theme.colors.text.primary)});
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const OptionValueWrapper = styled.div`
|
|
||||||
background: rgba(${props => props.theme.colors.bg.primary});
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 3px 6px 3px 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const OptionValueLabel = styled.span`
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(${props => props.theme.colors.text.secondary});
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const OptionValueRemove = styled.button`
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const InviteButton = styled(Button)`
|
|
||||||
margin-top: 12px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const InviteContainer = styled.div`
|
|
||||||
min-height: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
|
||||||
import * as S from './Styles';
|
|
||||||
|
|
||||||
type UserOptionProps = {
|
|
||||||
innerProps: any;
|
|
||||||
isDisabled: boolean;
|
|
||||||
isFocused: boolean;
|
|
||||||
label: string;
|
|
||||||
data: any;
|
|
||||||
getValue: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
|
|
||||||
return !isDisabled ? (
|
|
||||||
<S.OptionWrapper {...innerProps} isFocused={isFocused}>
|
|
||||||
<TaskAssignee
|
|
||||||
size={32}
|
|
||||||
member={{
|
|
||||||
id: '',
|
|
||||||
fullName: data.value.label,
|
|
||||||
profileIcon: data.value.profileIcon,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<S.OptionContent>
|
|
||||||
<S.OptionLabel fontSize={16} quiet={false}>
|
|
||||||
{label}
|
|
||||||
</S.OptionLabel>
|
|
||||||
{data.value.type === 2 && (
|
|
||||||
<S.OptionLabel fontSize={14} quiet>
|
|
||||||
Joined
|
|
||||||
</S.OptionLabel>
|
|
||||||
)}
|
|
||||||
</S.OptionContent>
|
|
||||||
</S.OptionWrapper>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserOption;
|
|
@ -1,82 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
import isValidEmail from 'shared/utils/email';
|
|
||||||
|
|
||||||
type MemberFilterOptions = {
|
|
||||||
projectID?: null | string;
|
|
||||||
teamID?: null | string;
|
|
||||||
organization?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function(client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) {
|
|
||||||
if (input && input.trim().length < 3) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const res = await client.query({
|
|
||||||
query: gql`
|
|
||||||
query {
|
|
||||||
searchMembers(input: {searchFilter:"${input}", projectID:"${projectID}"}) {
|
|
||||||
id
|
|
||||||
similarity
|
|
||||||
status
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
fullName
|
|
||||||
email
|
|
||||||
profileIcon {
|
|
||||||
url
|
|
||||||
initials
|
|
||||||
bgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
|
||||||
let results: any = [];
|
|
||||||
const emails: Array<string> = [];
|
|
||||||
if (res.data && res.data.searchMembers) {
|
|
||||||
results = [
|
|
||||||
...res.data.searchMembers.map((m: any) => {
|
|
||||||
if (m.status === 'INVITED') {
|
|
||||||
return {
|
|
||||||
label: m.id,
|
|
||||||
value: {
|
|
||||||
id: m.id,
|
|
||||||
type: 2,
|
|
||||||
profileIcon: {
|
|
||||||
bgColor: '#ccc',
|
|
||||||
initials: m.id.charAt(0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
emails.push(m.user.email);
|
|
||||||
return {
|
|
||||||
label: m.user.fullName,
|
|
||||||
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidEmail(input) && !emails.find(e => e === input)) {
|
|
||||||
results = [
|
|
||||||
...results,
|
|
||||||
{
|
|
||||||
label: input,
|
|
||||||
value: {
|
|
||||||
id: input,
|
|
||||||
type: 1,
|
|
||||||
profileIcon: {
|
|
||||||
bgColor: '#ccc',
|
|
||||||
initials: input.charAt(0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import AsyncSelect from 'react-select/async';
|
|
||||||
import { useApolloClient } from '@apollo/react-hooks';
|
|
||||||
import { colourStyles } from 'shared/components/Select';
|
|
||||||
import { Popup } from 'shared/components/PopupMenu';
|
|
||||||
import OptionValue from './OptionValue';
|
|
||||||
import UserOption from './UserOption';
|
|
||||||
import fetchMembers from './fetchMembers';
|
|
||||||
import * as S from './Styles';
|
|
||||||
|
|
||||||
type InviteUserData = {
|
|
||||||
email?: string;
|
|
||||||
userID?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UserManagementPopupProps = {
|
|
||||||
projectID: string;
|
|
||||||
users: Array<User>;
|
|
||||||
projectMembers: Array<TaskUser>;
|
|
||||||
onInviteProjectMembers: (data: Array<InviteUserData>) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({
|
|
||||||
projectID,
|
|
||||||
users,
|
|
||||||
projectMembers,
|
|
||||||
onInviteProjectMembers,
|
|
||||||
}) => {
|
|
||||||
const client = useApolloClient();
|
|
||||||
const [invitedUsers, setInvitedUsers] = useState<Array<any> | null>(null);
|
|
||||||
return (
|
|
||||||
<Popup tab={0} title="Invite a user">
|
|
||||||
<S.InviteContainer>
|
|
||||||
<AsyncSelect
|
|
||||||
getOptionValue={option => option.value.id}
|
|
||||||
placeholder="Email address or username"
|
|
||||||
noOptionsMessage={() => null}
|
|
||||||
onChange={(e: any) => {
|
|
||||||
setInvitedUsers(e);
|
|
||||||
}}
|
|
||||||
isMulti
|
|
||||||
autoFocus
|
|
||||||
cacheOptions
|
|
||||||
styles={colourStyles}
|
|
||||||
defaultOption
|
|
||||||
components={{
|
|
||||||
MultiValue: OptionValue,
|
|
||||||
Option: UserOption,
|
|
||||||
IndicatorSeparator: null,
|
|
||||||
DropdownIndicator: null,
|
|
||||||
}}
|
|
||||||
loadOptions={(i, cb) => fetchMembers(client, projectID, {}, i, cb)}
|
|
||||||
/>
|
|
||||||
</S.InviteContainer>
|
|
||||||
<S.InviteButton
|
|
||||||
onClick={() => {
|
|
||||||
if (invitedUsers) {
|
|
||||||
onInviteProjectMembers(
|
|
||||||
invitedUsers.map(user => {
|
|
||||||
if (user.value.type === 0) {
|
|
||||||
return {
|
|
||||||
userID: user.value.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
email: user.value.id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={invitedUsers === null}
|
|
||||||
hoverVariant="none"
|
|
||||||
fontSize="16px"
|
|
||||||
>
|
|
||||||
Send Invite
|
|
||||||
</S.InviteButton>
|
|
||||||
</Popup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserManagementPopup;
|
|
@ -1,9 +1,10 @@
|
|||||||
// LOC830
|
// LOC830
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, useContext } from 'react';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import GlobalTopNavbar from 'App/TopNavbar';
|
import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar';
|
||||||
import ProjectPopup from 'App/TopNavbar/ProjectPopup';
|
import styled from 'styled-components/macro';
|
||||||
import { usePopup } from 'shared/components/PopupMenu';
|
import AsyncSelect from 'react-select/async';
|
||||||
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import {
|
import {
|
||||||
useParams,
|
useParams,
|
||||||
Route,
|
Route,
|
||||||
@ -22,85 +23,432 @@ import {
|
|||||||
useFindProjectQuery,
|
useFindProjectQuery,
|
||||||
useDeleteInvitedProjectMemberMutation,
|
useDeleteInvitedProjectMemberMutation,
|
||||||
useUpdateTaskNameMutation,
|
useUpdateTaskNameMutation,
|
||||||
|
useCreateTaskMutation,
|
||||||
useDeleteTaskMutation,
|
useDeleteTaskMutation,
|
||||||
|
useUpdateTaskLocationMutation,
|
||||||
|
useUpdateTaskGroupLocationMutation,
|
||||||
|
useCreateTaskGroupMutation,
|
||||||
useUpdateTaskDescriptionMutation,
|
useUpdateTaskDescriptionMutation,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
FindProjectQuery,
|
FindProjectQuery,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
import UserContext, { useCurrentUser } from 'App/context';
|
||||||
|
import Input from 'shared/components/Input';
|
||||||
|
import Member from 'shared/components/Member';
|
||||||
|
import EmptyBoard from 'shared/components/EmptyBoard';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import useStateWithLocalStorage from 'shared/hooks/useStateWithLocalStorage';
|
import { Lock, Cross } from 'shared/icons';
|
||||||
import localStorage from 'shared/utils/localStorage';
|
import Button from 'shared/components/Button';
|
||||||
import polling from 'shared/utils/polling';
|
import { useApolloClient } from '@apollo/react-hooks';
|
||||||
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
import { colourStyles } from 'shared/components/Select';
|
||||||
import Board, { BoardLoading } from './Board';
|
import Board, { BoardLoading } from './Board';
|
||||||
import Details from './Details';
|
import Details from './Details';
|
||||||
import LabelManagerEditor from './LabelManagerEditor';
|
import LabelManagerEditor from './LabelManagerEditor';
|
||||||
import UserManagementPopup from './UserManagementPopup';
|
import { mixin } from '../../shared/utils/styles';
|
||||||
|
|
||||||
|
const CARD_LABEL_VARIANT_STORAGE_KEY = 'card_label_variant';
|
||||||
|
|
||||||
|
const RFC2822_EMAIL = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||||
|
|
||||||
|
const useStateWithLocalStorage = (localStorageKey: string): [string, React.Dispatch<React.SetStateAction<string>>] => {
|
||||||
|
const [value, setValue] = React.useState<string>(localStorage.getItem(localStorageKey) || '');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
localStorage.setItem(localStorageKey, value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return [value, setValue];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchInput = styled(Input)`
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserMember = styled(Member)`
|
||||||
|
padding: 4px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
|
||||||
|
}
|
||||||
|
border-radius: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberList = styled.div`
|
||||||
|
margin: 8px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type InviteUserData = {
|
||||||
|
email?: string;
|
||||||
|
suerID?: string;
|
||||||
|
};
|
||||||
|
type UserManagementPopupProps = {
|
||||||
|
projectID: string;
|
||||||
|
users: Array<User>;
|
||||||
|
projectMembers: Array<TaskUser>;
|
||||||
|
onInviteProjectMembers: (data: Array<InviteUserData>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VisibiltyPrivateIcon = styled(Lock)`
|
||||||
|
padding-right: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const VisibiltyButtonText = styled.span`
|
||||||
|
color: rgba(${props => props.theme.colors.text.primary});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ShareActions = styled.div`
|
||||||
|
border-top: 1px solid #414561;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const VisibiltyButton = styled.button`
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 2px 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
&:hover ${VisibiltyButtonText} {
|
||||||
|
color: rgba(${props => props.theme.colors.text.secondary});
|
||||||
|
}
|
||||||
|
&:hover ${VisibiltyPrivateIcon} {
|
||||||
|
fill: rgba(${props => props.theme.colors.text.secondary});
|
||||||
|
stroke: rgba(${props => props.theme.colors.text.secondary});
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
border-bottom: 1px solid rgba(${props => props.theme.colors.primary});
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type MemberFilterOptions = {
|
||||||
|
projectID?: null | string;
|
||||||
|
teamID?: null | string;
|
||||||
|
organization?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
|
||||||
|
if (input && input.trim().length < 3) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const res = await client.query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
searchMembers(input: {searchFilter:"${input}", projectID:"${projectID}"}) {
|
||||||
|
id
|
||||||
|
similarity
|
||||||
|
status
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
email
|
||||||
|
profileIcon {
|
||||||
|
url
|
||||||
|
initials
|
||||||
|
bgColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
let results: any = [];
|
||||||
|
const emails: Array<string> = [];
|
||||||
|
if (res.data && res.data.searchMembers) {
|
||||||
|
results = [
|
||||||
|
...res.data.searchMembers.map((m: any) => {
|
||||||
|
if (m.status === 'INVITED') {
|
||||||
|
return {
|
||||||
|
label: m.id,
|
||||||
|
value: {
|
||||||
|
id: m.id,
|
||||||
|
type: 2,
|
||||||
|
profileIcon: {
|
||||||
|
bgColor: '#ccc',
|
||||||
|
initials: m.id.charAt(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
emails.push(m.user.email);
|
||||||
|
return {
|
||||||
|
label: m.user.fullName,
|
||||||
|
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
|
||||||
|
results = [
|
||||||
|
...results,
|
||||||
|
{
|
||||||
|
label: input,
|
||||||
|
value: {
|
||||||
|
id: input,
|
||||||
|
type: 1,
|
||||||
|
profileIcon: {
|
||||||
|
bgColor: '#ccc',
|
||||||
|
initials: input.charAt(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserOptionProps = {
|
||||||
|
innerProps: any;
|
||||||
|
isDisabled: boolean;
|
||||||
|
isFocused: boolean;
|
||||||
|
label: string;
|
||||||
|
data: any;
|
||||||
|
getValue: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionWrapper = styled.div<{ isFocused: boolean }>`
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
${props => props.isFocused && `background: rgba(${props.theme.colors.primary});`}
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
const OptionContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${p => p.fontSize}px;
|
||||||
|
color: rgba(${p => (p.quiet ? p.theme.colors.text.primary : p.theme.colors.text.primary)});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
|
||||||
|
return !isDisabled ? (
|
||||||
|
<OptionWrapper {...innerProps} isFocused={isFocused}>
|
||||||
|
<TaskAssignee
|
||||||
|
size={32}
|
||||||
|
member={{
|
||||||
|
id: '',
|
||||||
|
fullName: data.value.label,
|
||||||
|
profileIcon: data.value.profileIcon,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<OptionContent>
|
||||||
|
<OptionLabel fontSize={16} quiet={false}>
|
||||||
|
{label}
|
||||||
|
</OptionLabel>
|
||||||
|
{data.value.type === 2 && (
|
||||||
|
<OptionLabel fontSize={14} quiet>
|
||||||
|
Joined
|
||||||
|
</OptionLabel>
|
||||||
|
)}
|
||||||
|
</OptionContent>
|
||||||
|
</OptionWrapper>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionValueWrapper = styled.div`
|
||||||
|
background: rgba(${props => props.theme.colors.bg.primary});
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 3px 6px 3px 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionValueLabel = styled.span`
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(${props => props.theme.colors.text.secondary});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionValueRemove = styled.button`
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
`;
|
||||||
|
const OptionValue = ({ data, removeProps }: any) => {
|
||||||
|
return (
|
||||||
|
<OptionValueWrapper>
|
||||||
|
<OptionValueLabel>{data.label}</OptionValueLabel>
|
||||||
|
<OptionValueRemove {...removeProps}>
|
||||||
|
<Cross width={14} height={14} />
|
||||||
|
</OptionValueRemove>
|
||||||
|
</OptionValueWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InviteButton = styled(Button)`
|
||||||
|
margin-top: 12px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InviteContainer = styled.div`
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({
|
||||||
|
projectID,
|
||||||
|
users,
|
||||||
|
projectMembers,
|
||||||
|
onInviteProjectMembers,
|
||||||
|
}) => {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const [invitedUsers, setInvitedUsers] = useState<Array<any> | null>(null);
|
||||||
|
return (
|
||||||
|
<Popup tab={0} title="Invite a user">
|
||||||
|
<InviteContainer>
|
||||||
|
<AsyncSelect
|
||||||
|
getOptionValue={option => option.value.id}
|
||||||
|
placeholder="Email address or username"
|
||||||
|
noOptionsMessage={() => null}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setInvitedUsers(e);
|
||||||
|
}}
|
||||||
|
isMulti
|
||||||
|
autoFocus
|
||||||
|
cacheOptions
|
||||||
|
styles={colourStyles}
|
||||||
|
defaultOption
|
||||||
|
components={{
|
||||||
|
MultiValue: OptionValue,
|
||||||
|
Option: UserOption,
|
||||||
|
IndicatorSeparator: null,
|
||||||
|
DropdownIndicator: null,
|
||||||
|
}}
|
||||||
|
loadOptions={(i, cb) => fetchMembers(client, projectID, {}, i, cb)}
|
||||||
|
/>
|
||||||
|
</InviteContainer>
|
||||||
|
<InviteButton
|
||||||
|
onClick={() => {
|
||||||
|
if (invitedUsers) {
|
||||||
|
onInviteProjectMembers(
|
||||||
|
invitedUsers.map(user => {
|
||||||
|
if (user.value.type === 0) {
|
||||||
|
return {
|
||||||
|
userID: user.value.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
email: user.value.id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={invitedUsers === null}
|
||||||
|
hoverVariant="none"
|
||||||
|
fontSize="16px"
|
||||||
|
>
|
||||||
|
Send Invite
|
||||||
|
</InviteButton>
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type TaskRouteProps = {
|
type TaskRouteProps = {
|
||||||
taskID: string;
|
taskID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface QuickCardEditorState {
|
||||||
|
isOpen: boolean;
|
||||||
|
target: React.RefObject<HTMLElement> | null;
|
||||||
|
taskID: string | null;
|
||||||
|
taskGroupID: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface ProjectParams {
|
interface ProjectParams {
|
||||||
projectID: string;
|
projectID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialQuickCardEditorState: QuickCardEditorState = {
|
||||||
|
taskID: null,
|
||||||
|
taskGroupID: null,
|
||||||
|
isOpen: false,
|
||||||
|
target: null,
|
||||||
|
};
|
||||||
|
|
||||||
const Project = () => {
|
const Project = () => {
|
||||||
const { projectID } = useParams<ProjectParams>();
|
const { projectID } = useParams<ProjectParams>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const { showPopup, hidePopup } = usePopup();
|
|
||||||
const labelsRef = useRef<Array<ProjectLabel>>([]);
|
|
||||||
const [value, setValue] = useStateWithLocalStorage(localStorage.CARD_LABEL_VARIANT_STORAGE_KEY);
|
|
||||||
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
|
|
||||||
|
|
||||||
const [updateTaskDescription] = useUpdateTaskDescriptionMutation();
|
const [updateTaskDescription] = useUpdateTaskDescriptionMutation();
|
||||||
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
|
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
|
||||||
const [updateTaskName] = useUpdateTaskNameMutation();
|
|
||||||
const { data, error } = useFindProjectQuery({
|
|
||||||
variables: { projectID },
|
|
||||||
pollInterval: polling.PROJECT,
|
|
||||||
});
|
|
||||||
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
const [toggleTaskLabel] = useToggleTaskLabelMutation({
|
||||||
onCompleted: (newTaskLabel) => {
|
onCompleted: newTaskLabel => {
|
||||||
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
taskLabelsRef.current = newTaskLabel.toggleTaskLabel.task.labels;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [value, setValue] = useStateWithLocalStorage(CARD_LABEL_VARIANT_STORAGE_KEY);
|
||||||
|
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
|
||||||
|
|
||||||
const [deleteTask] = useDeleteTaskMutation({
|
const [deleteTask] = useDeleteTaskMutation({
|
||||||
update: (client, resp) =>
|
update: (client, resp) =>
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (resp.data) {
|
if (resp.data) {
|
||||||
const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
|
const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
|
||||||
(tg) => tg.tasks.findIndex((t) => t.id === resp.data?.deleteTask.taskID) !== -1,
|
tg => tg.tasks.findIndex(t => t.id === resp.data?.deleteTask.taskID) !== -1,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (taskGroupIdx !== -1) {
|
if (taskGroupIdx !== -1) {
|
||||||
draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
|
draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
|
||||||
taskGroupIdx
|
taskGroupIdx
|
||||||
].tasks.filter((t) => t.id !== resp.data?.deleteTask.taskID);
|
].tasks.filter(t => t.id !== resp.data?.deleteTask.taskID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID: data ? data.findProject.id : '' },
|
{ projectID },
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [updateTaskName] = useUpdateTaskNameMutation();
|
||||||
|
|
||||||
|
const { loading, data, error } = useFindProjectQuery({
|
||||||
|
variables: { projectID },
|
||||||
|
pollInterval: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
const [updateProjectName] = useUpdateProjectNameMutation({
|
const [updateProjectName] = useUpdateProjectNameMutation({
|
||||||
update: (client, newName) => {
|
update: (client, newName) => {
|
||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
|
draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
|
||||||
}),
|
}),
|
||||||
{ projectID: data ? data.findProject.id : '' },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -110,8 +458,8 @@ const Project = () => {
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
draftCache.findProject.members = [
|
draftCache.findProject.members = [
|
||||||
...cache.findProject.members,
|
...cache.findProject.members,
|
||||||
@ -123,7 +471,7 @@ const Project = () => {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ projectID: data ? data.findProject.id : '' },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -132,13 +480,13 @@ const Project = () => {
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
|
draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
|
||||||
(m) => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
|
m => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID: data ? data.findProject.id : '' },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -147,23 +495,31 @@ const Project = () => {
|
|||||||
updateApolloCache<FindProjectQuery>(
|
updateApolloCache<FindProjectQuery>(
|
||||||
client,
|
client,
|
||||||
FindProjectDocument,
|
FindProjectDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findProject.members = cache.findProject.members.filter(
|
draftCache.findProject.members = cache.findProject.members.filter(
|
||||||
(m) => m.id !== response.data?.deleteProjectMember.member.id,
|
m => m.id !== response.data?.deleteProjectMember.member.id,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ projectID: data ? data.findProject.id : '' },
|
{ projectID },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { user } = useCurrentUser();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const { showPopup, hidePopup } = usePopup();
|
||||||
|
const $labelsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const labelsRef = useRef<Array<ProjectLabel>>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
document.title = `${data.findProject.name} | Taskcafé`;
|
document.title = `${data.findProject.name} | Taskcafé`;
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
if (error) {
|
||||||
|
history.push('/projects');
|
||||||
|
}
|
||||||
if (data) {
|
if (data) {
|
||||||
labelsRef.current = data.findProject.labels;
|
labelsRef.current = data.findProject.labels;
|
||||||
|
|
||||||
@ -171,29 +527,29 @@ const Project = () => {
|
|||||||
<>
|
<>
|
||||||
<GlobalTopNavbar
|
<GlobalTopNavbar
|
||||||
onChangeRole={(userID, roleCode) => {
|
onChangeRole={(userID, roleCode) => {
|
||||||
updateProjectMemberRole({ variables: { userID, roleCode, projectID: data ? data.findProject.id : '' } });
|
updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
|
||||||
}}
|
}}
|
||||||
onChangeProjectOwner={() => {
|
onChangeProjectOwner={uid => {
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onRemoveFromBoard={(userID) => {
|
onRemoveFromBoard={userID => {
|
||||||
deleteProjectMember({ variables: { userID, projectID: data ? data.findProject.id : '' } });
|
deleteProjectMember({ variables: { userID, projectID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onRemoveInvitedFromBoard={(email) => {
|
onRemoveInvitedFromBoard={email => {
|
||||||
deleteInvitedProjectMember({ variables: { projectID: data ? data.findProject.id : '', email } });
|
deleteInvitedProjectMember({ variables: { projectID, email } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
onSaveProjectName={(projectName) => {
|
onSaveProjectName={projectName => {
|
||||||
updateProjectName({ variables: { projectID: data ? data.findProject.id : '', name: projectName } });
|
updateProjectName({ variables: { projectID, name: projectName } });
|
||||||
}}
|
}}
|
||||||
onInviteUser={($target) => {
|
onInviteUser={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<UserManagementPopup
|
<UserManagementPopup
|
||||||
projectID={data ? data.findProject.id : ''}
|
projectID={projectID}
|
||||||
onInviteProjectMembers={(members) => {
|
onInviteProjectMembers={members => {
|
||||||
inviteProjectMembers({ variables: { projectID: data ? data.findProject.id : '', members } });
|
inviteProjectMembers({ variables: { projectID, members } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}}
|
}}
|
||||||
users={data.users}
|
users={data.users}
|
||||||
@ -201,14 +557,7 @@ const Project = () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
popupContent={
|
popupContent={<ProjectPopup history={history} name={data.findProject.name} projectID={projectID} />}
|
||||||
<ProjectPopup // eslint-disable-line
|
|
||||||
history={history}
|
|
||||||
publicOn={data.findProject.publicOn}
|
|
||||||
name={data.findProject.name}
|
|
||||||
projectID={projectID}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
menuType={[{ name: 'Board', link: location.pathname }]}
|
menuType={[{ name: 'Board', link: location.pathname }]}
|
||||||
currentTab={0}
|
currentTab={0}
|
||||||
projectMembers={data.findProject.members}
|
projectMembers={data.findProject.members}
|
||||||
@ -233,12 +582,12 @@ const Project = () => {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}/board/c/:taskID`}
|
path={`${match.path}/board/c/:taskID`}
|
||||||
render={() => {
|
render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
|
||||||
return (
|
|
||||||
<Details
|
<Details
|
||||||
refreshCache={NOOP}
|
refreshCache={NOOP}
|
||||||
availableMembers={data.findProject.members}
|
availableMembers={data.findProject.members}
|
||||||
projectURL={`${match.url}/board`}
|
projectURL={`${match.url}/board`}
|
||||||
|
taskID={routeProps.match.params.taskID}
|
||||||
onTaskNameChange={(updatedTask, newName) => {
|
onTaskNameChange={(updatedTask, newName) => {
|
||||||
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
|
updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
|
||||||
}}
|
}}
|
||||||
@ -255,7 +604,7 @@ const Project = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onDeleteTask={(deletedTask) => {
|
onDeleteTask={deletedTask => {
|
||||||
deleteTask({ variables: { taskID: deletedTask.id } });
|
deleteTask({ variables: { taskID: deletedTask.id } });
|
||||||
history.push(`${match.url}/board`);
|
history.push(`${match.url}/board`);
|
||||||
}}
|
}}
|
||||||
@ -264,19 +613,18 @@ const Project = () => {
|
|||||||
showPopup(
|
showPopup(
|
||||||
$targetRef,
|
$targetRef,
|
||||||
<LabelManagerEditor
|
<LabelManagerEditor
|
||||||
onLabelToggle={(labelID) => {
|
onLabelToggle={labelID => {
|
||||||
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
|
toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
|
||||||
}}
|
}}
|
||||||
taskID={task.id}
|
|
||||||
labelColors={data.labelColors}
|
labelColors={data.labelColors}
|
||||||
|
labels={labelsRef}
|
||||||
taskLabels={taskLabelsRef}
|
taskLabels={taskLabelsRef}
|
||||||
projectID={projectID}
|
projectID={projectID}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -9,23 +9,21 @@ import {
|
|||||||
GetProjectsDocument,
|
GetProjectsDocument,
|
||||||
GetProjectsQuery,
|
GetProjectsQuery,
|
||||||
} from 'shared/generated/graphql';
|
} from 'shared/generated/graphql';
|
||||||
import FormInput from 'shared/components/FormInput';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import NewProject from 'shared/components/NewProject';
|
import NewProject from 'shared/components/NewProject';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { PermissionLevel, PermissionObjectType, useCurrentUser } from 'App/context';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import ControlledInput from 'shared/components/ControlledInput';
|
import Input from 'shared/components/Input';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import theme from 'App/ThemeStyles';
|
import theme from 'App/ThemeStyles';
|
||||||
import polling from 'shared/utils/polling';
|
|
||||||
import { mixin } from '../shared/utils/styles';
|
import { mixin } from '../shared/utils/styles';
|
||||||
|
|
||||||
type CreateTeamData = { name: string };
|
type CreateTeamData = { teamName: string };
|
||||||
|
|
||||||
type CreateTeamFormProps = {
|
type CreateTeamFormProps = {
|
||||||
onCreateTeam: (teamName: string) => void;
|
onCreateTeam: (teamName: string) => void;
|
||||||
@ -37,30 +35,28 @@ const CreateTeamButton = styled(Button)`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ErrorText = styled.span`
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${(props) => props.theme.colors.danger};
|
|
||||||
`;
|
|
||||||
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
|
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
|
||||||
const {
|
const { register, handleSubmit } = useForm<CreateTeamData>();
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<CreateTeamData>();
|
|
||||||
const createTeam = (data: CreateTeamData) => {
|
const createTeam = (data: CreateTeamData) => {
|
||||||
onCreateTeam(data.name);
|
onCreateTeam(data.teamName);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<CreateTeamFormContainer onSubmit={handleSubmit(createTeam)}>
|
<CreateTeamFormContainer onSubmit={handleSubmit(createTeam)}>
|
||||||
{errors.name && <ErrorText>{errors.name.message}</ErrorText>}
|
<Input
|
||||||
<FormInput width="100%" label="Team name" variant="alternate" {...register('name')} />
|
width="100%"
|
||||||
|
label="Team name"
|
||||||
|
id="teamName"
|
||||||
|
name="teamName"
|
||||||
|
variant="alternate"
|
||||||
|
ref={register({ required: 'Team name is required' })}
|
||||||
|
/>
|
||||||
<CreateTeamButton type="submit">Create</CreateTeamButton>
|
<CreateTeamButton type="submit">Create</CreateTeamButton>
|
||||||
</CreateTeamFormContainer>
|
</CreateTeamFormContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectAddTile = styled.div`
|
const ProjectAddTile = styled.div`
|
||||||
background-color: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
|
background-color: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: 50%;
|
background-position: 50%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -74,7 +70,7 @@ const ProjectAddTile = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ProjectTile = styled(Link)<{ color: string }>`
|
const ProjectTile = styled(Link)<{ color: string }>`
|
||||||
background-color: ${(props) => props.color};
|
background-color: ${props => props.color};
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: 50%;
|
background-position: 50%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -145,7 +141,7 @@ const ProjectTileName = styled.div<{ centered?: boolean }>`
|
|||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
${(props) => props.centered && 'text-align: center;'}
|
${props => props.centered && 'text-align: center;'}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
@ -183,7 +179,7 @@ const SectionActionLink = styled(Link)`
|
|||||||
|
|
||||||
const ProjectSectionTitle = styled.h3`
|
const ProjectSectionTitle = styled.h3`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ProjectsContainer = styled.div`
|
const ProjectsContainer = styled.div`
|
||||||
@ -207,14 +203,14 @@ type ShowNewProject = {
|
|||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { showPopup, hidePopup } = usePopup();
|
const { showPopup, hidePopup } = usePopup();
|
||||||
const { loading, data } = useGetProjectsQuery({ pollInterval: polling.PROJECTS, fetchPolicy: 'cache-and-network' });
|
const { loading, data } = useGetProjectsQuery({ pollInterval: 3000, fetchPolicy: 'cache-and-network' });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'Taskcafé';
|
document.title = 'Taskcafé';
|
||||||
}, []);
|
}, []);
|
||||||
const [createProject] = useCreateProjectMutation({
|
const [createProject] = useCreateProjectMutation({
|
||||||
update: (client, newProject) => {
|
update: (client, newProject) => {
|
||||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, (cache) =>
|
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (newProject.data) {
|
if (newProject.data) {
|
||||||
draftCache.projects.push({ ...newProject.data.createProject });
|
draftCache.projects.push({ ...newProject.data.createProject });
|
||||||
}
|
}
|
||||||
@ -227,8 +223,8 @@ const Projects = () => {
|
|||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const [createTeam] = useCreateTeamMutation({
|
const [createTeam] = useCreateTeamMutation({
|
||||||
update: (client, createData) => {
|
update: (client, createData) => {
|
||||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, (cache) =>
|
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (createData.data) {
|
if (createData.data) {
|
||||||
draftCache.teams.push({ ...createData.data?.createTeam });
|
draftCache.teams.push({ ...createData.data?.createTeam });
|
||||||
}
|
}
|
||||||
@ -242,7 +238,7 @@ const Projects = () => {
|
|||||||
const { projects, teams, organizations } = data;
|
const { projects, teams, organizations } = data;
|
||||||
const organizationID = organizations[0].id ?? null;
|
const organizationID = organizations[0].id ?? null;
|
||||||
const personalProjects = projects
|
const personalProjects = projects
|
||||||
.filter((p) => p.team === null)
|
.filter(p => p.team === null)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const textA = a.name.toUpperCase();
|
const textA = a.name.toUpperCase();
|
||||||
const textB = b.name.toUpperCase();
|
const textB = b.name.toUpperCase();
|
||||||
@ -254,12 +250,12 @@ const Projects = () => {
|
|||||||
const textB = b.name.toUpperCase();
|
const textB = b.name.toUpperCase();
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0; // eslint-disable-line no-nested-ternary
|
return textA < textB ? -1 : textA > textB ? 1 : 0; // eslint-disable-line no-nested-ternary
|
||||||
})
|
})
|
||||||
.map((team) => {
|
.map(team => {
|
||||||
return {
|
return {
|
||||||
id: team.id,
|
id: team.id,
|
||||||
name: team.name,
|
name: team.name,
|
||||||
projects: projects
|
projects: projects
|
||||||
.filter((project) => project.team && project.team.id === team.id)
|
.filter(project => project.team && project.team.id === team.id)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const textA = a.name.toUpperCase();
|
const textA = a.name.toUpperCase();
|
||||||
const textB = b.name.toUpperCase();
|
const textB = b.name.toUpperCase();
|
||||||
@ -272,10 +268,10 @@ const Projects = () => {
|
|||||||
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
|
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ProjectsContainer>
|
<ProjectsContainer>
|
||||||
{true && ( // TODO: add permision check
|
{user.roles.org === 'admin' && (
|
||||||
<AddTeamButton
|
<AddTeamButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<Popup
|
<Popup
|
||||||
@ -286,7 +282,7 @@ const Projects = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CreateTeamForm
|
<CreateTeamForm
|
||||||
onCreateTeam={(teamName) => {
|
onCreateTeam={teamName => {
|
||||||
if (organizationID) {
|
if (organizationID) {
|
||||||
createTeam({ variables: { name: teamName, organizationID } });
|
createTeam({ variables: { name: teamName, organizationID } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
@ -307,7 +303,7 @@ const Projects = () => {
|
|||||||
<ProjectList>
|
<ProjectList>
|
||||||
{personalProjects.map((project, idx) => (
|
{personalProjects.map((project, idx) => (
|
||||||
<ProjectListItem key={project.id}>
|
<ProjectListItem key={project.id}>
|
||||||
<ProjectTile color={colors[idx % 5]} to={`/p/${project.shortId}`}>
|
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
|
||||||
<ProjectTileFade />
|
<ProjectTileFade />
|
||||||
<ProjectTileDetails>
|
<ProjectTileDetails>
|
||||||
<ProjectTileName>{project.name}</ProjectTileName>
|
<ProjectTileName>{project.name}</ProjectTileName>
|
||||||
@ -329,12 +325,12 @@ const Projects = () => {
|
|||||||
</ProjectListItem>
|
</ProjectListItem>
|
||||||
</ProjectList>
|
</ProjectList>
|
||||||
</div>
|
</div>
|
||||||
{projectTeams.map((team) => {
|
{projectTeams.map(team => {
|
||||||
return (
|
return (
|
||||||
<div key={team.id}>
|
<div key={team.id}>
|
||||||
<ProjectSectionTitleWrapper>
|
<ProjectSectionTitleWrapper>
|
||||||
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
||||||
{true && ( // TODO: add permision check
|
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||||
<SectionActions>
|
<SectionActions>
|
||||||
<SectionActionLink to={`/teams/${team.id}`}>
|
<SectionActionLink to={`/teams/${team.id}`}>
|
||||||
<SectionAction variant="outline">Projects</SectionAction>
|
<SectionAction variant="outline">Projects</SectionAction>
|
||||||
@ -351,7 +347,7 @@ const Projects = () => {
|
|||||||
<ProjectList>
|
<ProjectList>
|
||||||
{team.projects.map((project, idx) => (
|
{team.projects.map((project, idx) => (
|
||||||
<ProjectListItem key={project.id}>
|
<ProjectListItem key={project.id}>
|
||||||
<ProjectTile color={colors[idx % 5]} to={`/p/${project.shortId}`}>
|
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
|
||||||
<ProjectTileFade />
|
<ProjectTileFade />
|
||||||
<ProjectTileDetails>
|
<ProjectTileDetails>
|
||||||
<ProjectTileName>{project.name}</ProjectTileName>
|
<ProjectTileName>{project.name}</ProjectTileName>
|
||||||
@ -359,7 +355,7 @@ const Projects = () => {
|
|||||||
</ProjectTile>
|
</ProjectTile>
|
||||||
</ProjectListItem>
|
</ProjectListItem>
|
||||||
))}
|
))}
|
||||||
{true && ( // TODO: add permision check
|
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, team.id) && (
|
||||||
<ProjectListItem>
|
<ProjectListItem>
|
||||||
<ProjectAddTile
|
<ProjectAddTile
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -17,7 +17,6 @@ const UsersRegister = () => {
|
|||||||
<Register
|
<Register
|
||||||
registered={registered}
|
registered={registered}
|
||||||
onSubmit={(data, setComplete, setError) => {
|
onSubmit={(data, setComplete, setError) => {
|
||||||
let isRedirected = false;
|
|
||||||
if (data.password !== data.password_confirm) {
|
if (data.password !== data.password_confirm) {
|
||||||
setError('password', { type: 'error', message: 'Passwords must match' });
|
setError('password', { type: 'error', message: 'Passwords must match' });
|
||||||
setError('password_confirm', { type: 'error', message: 'Passwords must match' });
|
setError('password_confirm', { type: 'error', message: 'Passwords must match' });
|
||||||
@ -36,26 +35,23 @@ const UsersRegister = () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(async (x) => {
|
.then(async x => {
|
||||||
const response = await x.json();
|
const response = await x.json();
|
||||||
const { setup } = response;
|
const { setup } = response;
|
||||||
|
console.log(response);
|
||||||
if (setup) {
|
if (setup) {
|
||||||
history.replace(`/confirm?confirmToken=xxxx`);
|
history.replace(`/confirm?confirmToken=xxxx`);
|
||||||
isRedirected = true;
|
|
||||||
} else if (params.confirmToken) {
|
} else if (params.confirmToken) {
|
||||||
history.replace(`/confirm?confirmToken=${params.confirmToken}`);
|
history.replace(`/confirm?confirmToken=${params.confirmToken}`);
|
||||||
isRedirected = true;
|
|
||||||
} else {
|
} else {
|
||||||
setRegistered(true);
|
setRegistered(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(() => {
|
||||||
toast('There was an issue trying to register');
|
toast('There was an issue trying to register');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!isRedirected) {
|
|
||||||
setComplete(true);
|
setComplete(true);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</LoginWrapper>
|
</LoginWrapper>
|
||||||
|
@ -2,9 +2,8 @@ import React, { useState } from 'react';
|
|||||||
import Input from 'shared/components/Input';
|
import Input from 'shared/components/Input';
|
||||||
import updateApolloCache from 'shared/utils/cache';
|
import updateApolloCache from 'shared/utils/cache';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import polling from 'shared/utils/polling';
|
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { useCurrentUser, PermissionLevel, PermissionObjectType } from 'App/context';
|
||||||
import Select from 'shared/components/Select';
|
import Select from 'shared/components/Select';
|
||||||
import {
|
import {
|
||||||
useGetTeamQuery,
|
useGetTeamQuery,
|
||||||
@ -36,7 +35,7 @@ const UserMember = styled(Member)`
|
|||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
|
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.4)};
|
||||||
}
|
}
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
`;
|
`;
|
||||||
@ -57,8 +56,8 @@ const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, teamMe
|
|||||||
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
|
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
|
||||||
<TeamMemberList>
|
<TeamMemberList>
|
||||||
{users
|
{users
|
||||||
.filter((u) => u.id !== teamMembers.find((p) => p.id === u.id)?.id)
|
.filter(u => u.id !== teamMembers.find(p => p.id === u.id)?.id)
|
||||||
.map((user) => (
|
.map(user => (
|
||||||
<UserMember
|
<UserMember
|
||||||
key={user.id}
|
key={user.id}
|
||||||
onCardMemberClick={() => onAddTeamMember(user.id)}
|
onCardMemberClick={() => onAddTeamMember(user.id)}
|
||||||
@ -116,7 +115,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
${(props) =>
|
${props =>
|
||||||
props.disabled
|
props.disabled
|
||||||
? css`
|
? css`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -137,7 +136,7 @@ export const Content = styled.div`
|
|||||||
|
|
||||||
export const CurrentPermission = styled.span`
|
export const CurrentPermission = styled.span`
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
|
color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Separator = styled.div`
|
export const Separator = styled.div`
|
||||||
@ -148,13 +147,13 @@ export const Separator = styled.div`
|
|||||||
|
|
||||||
export const WarningText = styled.span`
|
export const WarningText = styled.span`
|
||||||
display: flex;
|
display: flex;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DeleteDescription = styled.div`
|
export const DeleteDescription = styled.div`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RemoveMemberButton = styled(Button)`
|
export const RemoveMemberButton = styled(Button)`
|
||||||
@ -221,13 +220,13 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
<MiniProfileActions>
|
<MiniProfileActions>
|
||||||
<MiniProfileActionWrapper>
|
<MiniProfileActionWrapper>
|
||||||
{permissions
|
{permissions
|
||||||
.filter((p) => (subject.role && subject.role.code === 'owner') || p.code !== 'owner')
|
.filter(p => (subject.role && subject.role.code === 'owner') || p.code !== 'owner')
|
||||||
.map((perm) => (
|
.map(perm => (
|
||||||
<MiniProfileActionItem
|
<MiniProfileActionItem
|
||||||
disabled={subject.role && perm.code !== subject.role.code && !canChangeRole}
|
disabled={subject.role && perm.code !== subject.role.code && !canChangeRole}
|
||||||
key={perm.code}
|
key={perm.code}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (subject.role && perm.code !== subject.role.code) {
|
if (onChangeRole && subject.role && perm.code !== subject.role.code) {
|
||||||
switch (perm.code) {
|
switch (perm.code) {
|
||||||
case 'owner':
|
case 'owner':
|
||||||
onChangeRole(RoleCode.Owner);
|
onChangeRole(RoleCode.Owner);
|
||||||
@ -276,8 +275,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
<Select
|
<Select
|
||||||
label="New projects owner"
|
label="New projects owner"
|
||||||
value={orphanedProjectOwner}
|
value={orphanedProjectOwner}
|
||||||
onChange={(value) => setOrphanedProjectOwner(value)}
|
onChange={value => setOrphanedProjectOwner(value)}
|
||||||
options={members.filter((m) => m.id !== subject.id).map((m) => ({ label: m.fullName, value: m.id }))}
|
options={members.filter(m => m.id !== subject.id).map(m => ({ label: m.fullName, value: m.id }))}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -307,14 +306,14 @@ const MemberItemOption = styled(Button)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberList = styled.div`
|
const MemberList = styled.div`
|
||||||
border-top: 1px solid ${(props) => props.theme.colors.border};
|
border-top: 1px solid ${props => props.theme.colors.border};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberListItem = styled.div`
|
const MemberListItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid ${(props) => props.theme.colors.border};
|
border-bottom: 1px solid ${props => props.theme.colors.border};
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 12px 0 12px 40px;
|
padding: 12px 0 12px 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -338,11 +337,11 @@ const MemberProfile = styled(TaskAssignee)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberItemName = styled.p`
|
const MemberItemName = styled.p`
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberItemUsername = styled.p`
|
const MemberItemUsername = styled.p`
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MemberListHeader = styled.div`
|
const MemberListHeader = styled.div`
|
||||||
@ -351,12 +350,12 @@ const MemberListHeader = styled.div`
|
|||||||
`;
|
`;
|
||||||
const ListTitle = styled.h3`
|
const ListTitle = styled.h3`
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
`;
|
`;
|
||||||
const ListDesc = styled.span`
|
const ListDesc = styled.span`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
const FilterSearch = styled(Input)`
|
const FilterSearch = styled(Input)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -388,11 +387,11 @@ const FilterTabItem = styled.li`
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
&:hover {
|
&:hover {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -423,9 +422,9 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
const { loading, data } = useGetTeamQuery({
|
const { loading, data } = useGetTeamQuery({
|
||||||
variables: { teamID },
|
variables: { teamID },
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
pollInterval: polling.MEMBERS,
|
pollInterval: 3000,
|
||||||
});
|
});
|
||||||
const { user } = useCurrentUser();
|
const { user, setUserRoles } = useCurrentUser();
|
||||||
const warning =
|
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”.';
|
'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 [createTeamMember] = useCreateTeamMemberMutation({
|
const [createTeamMember] = useCreateTeamMemberMutation({
|
||||||
@ -433,8 +432,8 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
updateApolloCache<GetTeamQuery>(
|
updateApolloCache<GetTeamQuery>(
|
||||||
client,
|
client,
|
||||||
GetTeamDocument,
|
GetTeamDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
draftCache.findTeam.members.push({
|
draftCache.findTeam.members.push({
|
||||||
...response.data.createTeamMember.teamMember,
|
...response.data.createTeamMember.teamMember,
|
||||||
@ -447,16 +446,26 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation();
|
const [updateTeamMemberRole] = useUpdateTeamMemberRoleMutation({
|
||||||
|
onCompleted: r => {
|
||||||
|
if (user) {
|
||||||
|
setUserRoles(
|
||||||
|
produce(user.roles, draftRoles => {
|
||||||
|
draftRoles.teams.set(r.updateTeamMemberRole.teamID, r.updateTeamMemberRole.member.role.code);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
const [deleteTeamMember] = useDeleteTeamMemberMutation({
|
||||||
update: (client, response) => {
|
update: (client, response) => {
|
||||||
updateApolloCache<GetTeamQuery>(
|
updateApolloCache<GetTeamQuery>(
|
||||||
client,
|
client,
|
||||||
GetTeamDocument,
|
GetTeamDocument,
|
||||||
(cache) =>
|
cache =>
|
||||||
produce(cache, (draftCache) => {
|
produce(cache, draftCache => {
|
||||||
draftCache.findTeam.members = cache.findTeam.members.filter(
|
draftCache.findTeam.members = cache.findTeam.members.filter(
|
||||||
(member) => member.id !== response.data?.deleteTeamMember.userID,
|
member => member.id !== response.data?.deleteTeamMember.userID,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ teamID },
|
{ teamID },
|
||||||
@ -482,15 +491,15 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
</ListDesc>
|
</ListDesc>
|
||||||
<ListActions>
|
<ListActions>
|
||||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||||
{true && ( // TODO: add permission check
|
{user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, data.findTeam.id) && (
|
||||||
<InviteMemberButton
|
<InviteMemberButton
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<UserManagementPopup
|
<UserManagementPopup
|
||||||
users={data.users}
|
users={data.users}
|
||||||
teamMembers={data.findTeam.members}
|
teamMembers={data.findTeam.members}
|
||||||
onAddTeamMember={(userID) => {
|
onAddTeamMember={userID => {
|
||||||
createTeamMember({ variables: { userID, teamID } });
|
createTeamMember({ variables: { userID, teamID } });
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
@ -504,7 +513,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
</ListActions>
|
</ListActions>
|
||||||
</MemberListHeader>
|
</MemberListHeader>
|
||||||
<MemberList>
|
<MemberList>
|
||||||
{data.findTeam.members.map((member) => (
|
{data.findTeam.members.map(member => (
|
||||||
<MemberListItem>
|
<MemberListItem>
|
||||||
<MemberProfile showRoleIcons size={32} onMemberProfile={NOOP} member={member} />
|
<MemberProfile showRoleIcons size={32} onMemberProfile={NOOP} member={member} />
|
||||||
<MemberListItemDetails>
|
<MemberListItemDetails>
|
||||||
@ -515,23 +524,22 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
|
|||||||
<MemberItemOption variant="flat">On 2 projects</MemberItemOption>
|
<MemberItemOption variant="flat">On 2 projects</MemberItemOption>
|
||||||
<MemberItemOption
|
<MemberItemOption
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
currentUserID={user ?? ''}
|
currentUserID={user.id ?? ''}
|
||||||
subject={member}
|
subject={member}
|
||||||
members={data.findTeam.members}
|
members={data.findTeam.members}
|
||||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||||
// canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)} TODO: add permission check
|
canChangeRole={user.isAdmin(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)}
|
||||||
canChangeRole
|
onChangeRole={roleCode => {
|
||||||
onChangeRole={(roleCode) => {
|
|
||||||
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
updateTeamMemberRole({ variables: { userID: member.id, teamID, roleCode } });
|
||||||
}}
|
}}
|
||||||
onRemoveFromTeam={
|
onRemoveFromTeam={
|
||||||
member.role && member.role.code === 'owner'
|
member.role && member.role.code === 'owner'
|
||||||
? undefined
|
? undefined
|
||||||
: (newOwnerID) => {
|
: newOwnerID => {
|
||||||
deleteTeamMember({ variables: { teamID, newOwnerID, userID: member.id } });
|
deleteTeamMember({ variables: { teamID, newOwnerID, userID: member.id } });
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Input from 'shared/components/Input';
|
import Input from 'shared/components/Input';
|
||||||
import theme from 'App/ThemeStyles';
|
import theme from 'App/ThemeStyles';
|
||||||
import polling from 'shared/utils/polling';
|
|
||||||
|
|
||||||
const FilterSearch = styled(Input)`
|
const FilterSearch = styled(Input)`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -159,7 +158,7 @@ const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
|
|||||||
const { loading, data } = useGetTeamQuery({
|
const { loading, data } = useGetTeamQuery({
|
||||||
variables: { teamID },
|
variables: { teamID },
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
pollInterval: polling.TEAM_PROJECTS,
|
pollInterval: 3000,
|
||||||
});
|
});
|
||||||
if (data) {
|
if (data) {
|
||||||
return (
|
return (
|
||||||
|
@ -13,7 +13,7 @@ import { usePopup, Popup } from 'shared/components/PopupMenu';
|
|||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
|
||||||
import { useCurrentUser } from 'App/context';
|
import { PermissionObjectType, PermissionLevel, useCurrentUser } from 'App/context';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import Members from './Members';
|
import Members from './Members';
|
||||||
import Projects from './Projects';
|
import Projects from './Projects';
|
||||||
@ -95,12 +95,9 @@ const Teams = () => {
|
|||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
if (data && user) {
|
if (data && user) {
|
||||||
/*
|
|
||||||
TODO: re-add permission check
|
|
||||||
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalTopNavbar
|
<GlobalTopNavbar
|
||||||
|
@ -1,35 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { ApolloClient } from '@apollo/client';
|
import axios from 'axios';
|
||||||
import { ApolloProvider } from '@apollo/client/react';
|
import createAuthRefreshInterceptor from 'axios-auth-refresh';
|
||||||
|
import { ApolloProvider } from '@apollo/react-hooks';
|
||||||
|
import { ApolloClient } from 'apollo-client';
|
||||||
|
import { HttpLink } from 'apollo-link-http';
|
||||||
|
import { onError } from 'apollo-link-error';
|
||||||
import { enableMapSet } from 'immer';
|
import { enableMapSet } from 'immer';
|
||||||
|
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import updateLocale from 'dayjs/plugin/updateLocale';
|
import updateLocale from 'dayjs/plugin/updateLocale';
|
||||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
import isBetween from 'dayjs/plugin/isBetween';
|
import isBetween from 'dayjs/plugin/isBetween';
|
||||||
import weekday from 'dayjs/plugin/weekday';
|
import weekday from 'dayjs/plugin/weekday';
|
||||||
import duration from 'dayjs/plugin/duration';
|
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import log from 'loglevel';
|
|
||||||
import remote from 'loglevel-plugin-remote';
|
|
||||||
import cache from './App/cache';
|
import cache from './App/cache';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
if (process.env.REACT_APP_NODE_ENV === 'production') {
|
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
|
||||||
remote.apply(log, { format: remote.json });
|
|
||||||
switch (process.env.REACT_APP_LOG_LEVEL) {
|
|
||||||
case 'info':
|
|
||||||
log.setLevel(log.levels.INFO);
|
|
||||||
break;
|
|
||||||
case 'debug':
|
|
||||||
log.setLevel(log.levels.DEBUG);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log.setLevel(log.levels.ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
|
||||||
@ -38,8 +27,6 @@ dayjs.extend(weekday);
|
|||||||
dayjs.extend(isBetween);
|
dayjs.extend(isBetween);
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
dayjs.extend(updateLocale);
|
dayjs.extend(updateLocale);
|
||||||
dayjs.extend(duration);
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
dayjs.updateLocale('en', {
|
dayjs.updateLocale('en', {
|
||||||
week: {
|
week: {
|
||||||
dow: 1, // First day of week is Monday
|
dow: 1, // First day of week is Monday
|
||||||
@ -47,7 +34,131 @@ dayjs.updateLocale('en', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new ApolloClient({ uri: '/graphql', cache });
|
let forward$;
|
||||||
|
let isRefreshing = false;
|
||||||
|
let pendingRequests: any = [];
|
||||||
|
|
||||||
|
const refreshAuthLogic = (failedRequest: any) =>
|
||||||
|
axios.post('/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => {
|
||||||
|
setAccessToken(tokenRefreshResponse.data.accessToken);
|
||||||
|
failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.data.accessToken}`;
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
createAuthRefreshInterceptor(axios, refreshAuthLogic);
|
||||||
|
|
||||||
|
const resolvePendingRequests = () => {
|
||||||
|
pendingRequests.map((callback: any) => callback());
|
||||||
|
pendingRequests = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvePromise = (resolve: () => void) => {
|
||||||
|
pendingRequests.push(() => resolve());
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPendingRequests = () => {
|
||||||
|
pendingRequests = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const setRefreshing = (newVal: boolean) => {
|
||||||
|
isRefreshing = newVal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
||||||
|
if (graphQLErrors) {
|
||||||
|
for (const err of graphQLErrors) {
|
||||||
|
if (err.extensions && err.extensions.code) {
|
||||||
|
switch (err.extensions.code) {
|
||||||
|
case 'UNAUTHENTICATED':
|
||||||
|
if (!isRefreshing) {
|
||||||
|
setRefreshing(true);
|
||||||
|
forward$ = fromPromise(
|
||||||
|
getNewToken()
|
||||||
|
.then((response: any) => {
|
||||||
|
setAccessToken(response.accessToken);
|
||||||
|
resolvePendingRequests();
|
||||||
|
return response.accessToken;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
resetPendingRequests();
|
||||||
|
// TODO
|
||||||
|
// Handle token refresh errors e.g clear stored tokens, redirect to login, ...
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setRefreshing(false);
|
||||||
|
}),
|
||||||
|
).filter(value => Boolean(value));
|
||||||
|
} else {
|
||||||
|
forward$ = fromPromise(new Promise(resolvePromise));
|
||||||
|
}
|
||||||
|
return forward$.flatMap(() => forward(operation));
|
||||||
|
default:
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (networkError) {
|
||||||
|
console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestLink = new ApolloLink(
|
||||||
|
(operation, forward) =>
|
||||||
|
new Observable((observer: any) => {
|
||||||
|
let handle: any;
|
||||||
|
Promise.resolve(operation)
|
||||||
|
.then((op: any) => {
|
||||||
|
const accessToken = getAccessToken();
|
||||||
|
if (accessToken) {
|
||||||
|
op.setContext({
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
handle = forward(operation).subscribe({
|
||||||
|
next: observer.next.bind(observer),
|
||||||
|
error: observer.error.bind(observer),
|
||||||
|
complete: observer.complete.bind(observer),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(observer.error.bind(observer));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (handle) {
|
||||||
|
handle.unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = new ApolloClient({
|
||||||
|
link: ApolloLink.from([
|
||||||
|
onError(({ graphQLErrors, networkError }) => {
|
||||||
|
if (graphQLErrors) {
|
||||||
|
graphQLErrors.forEach(
|
||||||
|
({ message, locations, path }) =>
|
||||||
|
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`), // eslint-disable-line no-console
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (networkError) {
|
||||||
|
console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
errorLink,
|
||||||
|
requestLink,
|
||||||
|
new HttpLink({
|
||||||
|
uri: '/graphql',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
cache,
|
||||||
|
});
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import TextareaAutosize from 'react-autosize-textarea';
|
import TextareaAutosize from 'react-autosize-textarea/lib';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
|
|
||||||
|
@ -49,7 +49,6 @@ export const NameEditor: React.FC<NameEditorProps> = ({ onSave: handleSave, onCa
|
|||||||
<ListNameEditorWrapper>
|
<ListNameEditorWrapper>
|
||||||
<ListNameEditor
|
<ListNameEditor
|
||||||
ref={$editorRef}
|
ref={$editorRef}
|
||||||
height={40}
|
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
value={listName}
|
value={listName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setListName(e.currentTarget.value)}
|
||||||
|
@ -10,215 +10,6 @@ import Button from 'shared/components/Button';
|
|||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
const UserSelect = styled(Select)`
|
|
||||||
margin: 8px 0;
|
|
||||||
padding: 8px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NewUserPassInput = styled(Input)`
|
|
||||||
margin: 8px 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InviteMemberButton = styled(Button)`
|
|
||||||
padding: 7px 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UserPassBar = styled.div`
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberItemOptions = styled.div``;
|
|
||||||
|
|
||||||
const MemberItemOption = styled(Button)`
|
|
||||||
padding: 7px 9px;
|
|
||||||
margin: 4px 0 4px 8px;
|
|
||||||
float: left;
|
|
||||||
min-width: 95px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberList = styled.div`
|
|
||||||
border-top: 1px solid ${(props) => props.theme.colors.border};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListItem = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid ${(props) => props.theme.colors.border};
|
|
||||||
min-height: 40px;
|
|
||||||
padding: 12px 0 12px 40px;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListItemDetails = styled.div`
|
|
||||||
float: left;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
padding-left: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InviteIcon = styled(UserPlus)`
|
|
||||||
padding-right: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberProfile = styled(TaskAssignee)`
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: 0;
|
|
||||||
margin: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberItemName = styled.p`
|
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberItemUsername = styled.p`
|
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListHeader = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
const ListTitle = styled.h3`
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
|
||||||
margin-bottom: 12px;
|
|
||||||
`;
|
|
||||||
const ListDesc = styled.span`
|
|
||||||
font-size: 16px;
|
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
|
||||||
`;
|
|
||||||
const FilterSearch = styled(Input)`
|
|
||||||
margin: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ListActions = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberListWrapper = styled.div`
|
|
||||||
flex: 1 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
padding: 2.2rem;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1400px;
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNav = styled.div`
|
|
||||||
float: left;
|
|
||||||
width: 220px;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavContent = styled.ul`
|
|
||||||
display: block;
|
|
||||||
width: auto;
|
|
||||||
border-bottom: 0 !important;
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavItem = styled.li`
|
|
||||||
padding: 0.35rem 0.3rem;
|
|
||||||
height: 48px;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavItemButton = styled.button<{ active: boolean }>`
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
padding-top: 10px !important;
|
|
||||||
padding-bottom: 10px !important;
|
|
||||||
padding-left: 12px !important;
|
|
||||||
padding-right: 8px !important;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
color: ${(props) => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
|
|
||||||
&:hover {
|
|
||||||
color: ${(props) => `${props.theme.colors.primary}`};
|
|
||||||
}
|
|
||||||
&:hover svg {
|
|
||||||
fill: ${(props) => props.theme.colors.primary};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const TabItemUser = styled(User)<{ active: boolean }>`
|
|
||||||
fill: ${(props) => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
|
||||||
stroke: ${(props) => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavItemSpan = styled.span`
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 9px;
|
|
||||||
font-size: 14px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabNavLine = styled.span<{ top: number }>`
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
width: 2px;
|
|
||||||
height: 48px;
|
|
||||||
transform: scaleX(1);
|
|
||||||
top: ${(props) => props.top}px;
|
|
||||||
|
|
||||||
background: linear-gradient(
|
|
||||||
30deg,
|
|
||||||
${(props) => props.theme.colors.primary},
|
|
||||||
${(props) => props.theme.colors.primary}
|
|
||||||
);
|
|
||||||
box-shadow: 0 0 8px 0 ${(props) => props.theme.colors.primary};
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabContentWrapper = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TabContent = styled.div`
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
padding: 1.5rem;
|
|
||||||
background-color: #10163a;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const items = [{ name: 'Members' }];
|
|
||||||
|
|
||||||
export const RoleCheckmark = styled(Checkmark)`
|
export const RoleCheckmark = styled(Checkmark)`
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
`;
|
`;
|
||||||
@ -263,7 +54,7 @@ export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
|
|||||||
position: relative;
|
position: relative;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
${(props) =>
|
${props =>
|
||||||
props.disabled
|
props.disabled
|
||||||
? css`
|
? css`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -284,7 +75,7 @@ export const Content = styled.div`
|
|||||||
|
|
||||||
export const CurrentPermission = styled.span`
|
export const CurrentPermission = styled.span`
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
|
color: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Separator = styled.div`
|
export const Separator = styled.div`
|
||||||
@ -295,13 +86,13 @@ export const Separator = styled.div`
|
|||||||
|
|
||||||
export const WarningText = styled.span`
|
export const WarningText = styled.span`
|
||||||
display: flex;
|
display: flex;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.4)};
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DeleteDescription = styled.div`
|
export const DeleteDescription = styled.div`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RemoveMemberButton = styled(Button)`
|
export const RemoveMemberButton = styled(Button)`
|
||||||
@ -370,8 +161,8 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
<MiniProfileActions>
|
<MiniProfileActions>
|
||||||
<MiniProfileActionWrapper>
|
<MiniProfileActionWrapper>
|
||||||
{permissions
|
{permissions
|
||||||
.filter((p) => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
|
||||||
.map((perm) => (
|
.map(perm => (
|
||||||
<MiniProfileActionItem
|
<MiniProfileActionItem
|
||||||
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
|
||||||
key={perm.code}
|
key={perm.code}
|
||||||
@ -422,9 +213,9 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
Choose a new user to take over ownership of the users teams & projects.
|
Choose a new user to take over ownership of the users teams & projects.
|
||||||
</DeleteDescription>
|
</DeleteDescription>
|
||||||
<UserSelect
|
<UserSelect
|
||||||
onChange={(v) => setDeleteUser(v)}
|
onChange={v => setDeleteUser(v)}
|
||||||
value={deleteUser}
|
value={deleteUser}
|
||||||
options={users.map((u) => ({ label: u.fullName, value: u.id }))}
|
options={users.map(u => ({ label: u.fullName, value: u.id }))}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -449,7 +240,7 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
Removing this user from the organzation will remove them from assigned tasks, projects, and teams.
|
||||||
</DeleteDescription>
|
</DeleteDescription>
|
||||||
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
|
<DeleteDescription>{`The user is the owner of ${user.owned.projects.length} projects & ${user.owned.teams.length} teams.`}</DeleteDescription>
|
||||||
<UserSelect onChange={NOOP} value={null} options={users.map((u) => ({ label: u.fullName, value: u.id }))} />
|
<UserSelect onChange={NOOP} value={null} options={users.map(u => ({ label: u.fullName, value: u.id }))} />
|
||||||
<UserPassConfirmButton
|
<UserPassConfirmButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// onDeleteUser();
|
// onDeleteUser();
|
||||||
@ -502,6 +293,211 @@ const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UserSelect = styled(Select)`
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NewUserPassInput = styled(Input)`
|
||||||
|
margin: 8px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InviteMemberButton = styled(Button)`
|
||||||
|
padding: 7px 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserPassBar = styled.div`
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberItemOptions = styled.div``;
|
||||||
|
|
||||||
|
const MemberItemOption = styled(Button)`
|
||||||
|
padding: 7px 9px;
|
||||||
|
margin: 4px 0 4px 8px;
|
||||||
|
float: left;
|
||||||
|
min-width: 95px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberList = styled.div`
|
||||||
|
border-top: 1px solid ${props => props.theme.colors.border};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListItem = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid ${props => props.theme.colors.border};
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 12px 0 12px 40px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListItemDetails = styled.div`
|
||||||
|
float: left;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InviteIcon = styled(UserPlus)`
|
||||||
|
padding-right: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberProfile = styled(TaskAssignee)`
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberItemName = styled.p`
|
||||||
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberItemUsername = styled.p`
|
||||||
|
color: ${props => props.theme.colors.text.primary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
const ListTitle = styled.h3`
|
||||||
|
font-size: 18px;
|
||||||
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
|
margin-bottom: 12px;
|
||||||
|
`;
|
||||||
|
const ListDesc = styled.span`
|
||||||
|
font-size: 16px;
|
||||||
|
color: ${props => props.theme.colors.text.primary};
|
||||||
|
`;
|
||||||
|
const FilterSearch = styled(Input)`
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ListActions = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberListWrapper = styled.div`
|
||||||
|
flex: 1 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
padding: 2.2rem;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNav = styled.div`
|
||||||
|
float: left;
|
||||||
|
width: 220px;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavContent = styled.ul`
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavItem = styled.li`
|
||||||
|
padding: 0.35rem 0.3rem;
|
||||||
|
height: 48px;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavItemButton = styled.button<{ active: boolean }>`
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding-top: 10px !important;
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
padding-left: 12px !important;
|
||||||
|
padding-right: 8px !important;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
color: ${props => (props.active ? `${props.theme.colors.secondary}` : props.theme.colors.text.primary)};
|
||||||
|
&:hover {
|
||||||
|
color: ${props => `${props.theme.colors.primary}`};
|
||||||
|
}
|
||||||
|
&:hover svg {
|
||||||
|
fill: ${props => props.theme.colors.primary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const TabItemUser = styled(User)<{ active: boolean }>`
|
||||||
|
fill: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
||||||
|
stroke: ${props => (props.active ? `${props.theme.colors.primary}` : props.theme.colors.text.primary)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavItemSpan = styled.span`
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 9px;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabNavLine = styled.span<{ top: number }>`
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
width: 2px;
|
||||||
|
height: 48px;
|
||||||
|
transform: scaleX(1);
|
||||||
|
top: ${props => props.top}px;
|
||||||
|
|
||||||
|
background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
|
||||||
|
box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary};
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabContentWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TabContent = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: #10163a;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const items = [{ name: 'Members' }];
|
||||||
|
|
||||||
type NavItemProps = {
|
type NavItemProps = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
@ -595,7 +591,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
|
||||||
{canInviteUser && (
|
{canInviteUser && (
|
||||||
<InviteMemberButton
|
<InviteMemberButton
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
onAddUser($target);
|
onAddUser($target);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -606,7 +602,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
</ListActions>
|
</ListActions>
|
||||||
</MemberListHeader>
|
</MemberListHeader>
|
||||||
<MemberList>
|
<MemberList>
|
||||||
{users.map((member) => {
|
{users.map(member => {
|
||||||
const projectTotal = member.owned.projects.length + member.member.projects.length;
|
const projectTotal = member.owned.projects.length + member.member.projects.length;
|
||||||
return (
|
return (
|
||||||
<MemberListItem>
|
<MemberListItem>
|
||||||
@ -619,7 +615,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
|
<MemberItemOption variant="flat">{`On ${projectTotal} projects`}</MemberItemOption>
|
||||||
<MemberItemOption
|
<MemberItemOption
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
@ -630,7 +626,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
onUpdateUserPassword(user, password);
|
onUpdateUserPassword(user, password);
|
||||||
}}
|
}}
|
||||||
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
|
canChangeRole={(member.role && member.role.code !== 'owner') ?? false}
|
||||||
onChangeRole={(roleCode) => {
|
onChangeRole={roleCode => {
|
||||||
updateUserRole({ variables: { userID: member.id, roleCode } });
|
updateUserRole({ variables: { userID: member.id, roleCode } });
|
||||||
}}
|
}}
|
||||||
onDeleteUser={onDeleteUser}
|
onDeleteUser={onDeleteUser}
|
||||||
@ -644,7 +640,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
</MemberListItem>
|
</MemberListItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{invitedUsers.map((member) => {
|
{invitedUsers.map(member => {
|
||||||
return (
|
return (
|
||||||
<MemberListItem>
|
<MemberListItem>
|
||||||
<MemberProfile
|
<MemberProfile
|
||||||
@ -668,7 +664,7 @@ const Admin: React.FC<AdminProps> = ({
|
|||||||
<MemberItemOptions>
|
<MemberItemOptions>
|
||||||
<MemberItemOption
|
<MemberItemOption
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
showPopup(
|
showPopup(
|
||||||
$target,
|
$target,
|
||||||
<TeamRoleManagerPopup
|
<TeamRoleManagerPopup
|
||||||
|
@ -6,11 +6,11 @@ const Text = styled.span<{ fontSize: string; justifyTextContent: string; hasIcon
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: ${(props) => props.justifyTextContent};
|
justify-content: ${props => props.justifyTextContent};
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
font-size: ${(props) => props.fontSize};
|
font-size: ${props => props.fontSize};
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
${(props) =>
|
${props =>
|
||||||
props.hasIcon &&
|
props.hasIcon &&
|
||||||
css`
|
css`
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
@ -23,11 +23,11 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.75rem 2rem;
|
padding: 0.75rem 2rem;
|
||||||
border-radius: ${(props) => props.theme.borderRadius.alternate};
|
border-radius: ${props => props.theme.borderRadius.alternate};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
${(props) =>
|
${props =>
|
||||||
props.disabled &&
|
props.disabled &&
|
||||||
css`
|
css`
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -37,8 +37,8 @@ const Base = styled.button<{ color: string; disabled: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
|
const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
|
||||||
background: ${(props) => props.theme.colors[props.color]};
|
background: ${props => props.theme.colors[props.color]};
|
||||||
${(props) =>
|
${props =>
|
||||||
props.hoverVariant === 'boxShadow' &&
|
props.hoverVariant === 'boxShadow' &&
|
||||||
css`
|
css`
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -48,9 +48,9 @@ const Filled = styled(Base)<{ hoverVariant: HoverVariant }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Outline = styled(Base)<{ invert: boolean }>`
|
const Outline = styled(Base)<{ invert: boolean }>`
|
||||||
border: 1px solid ${(props) => props.theme.colors[props.color]};
|
border: 1px solid ${props => props.theme.colors[props.color]};
|
||||||
background: transparent;
|
background: transparent;
|
||||||
${(props) =>
|
${props =>
|
||||||
props.invert
|
props.invert
|
||||||
? css`
|
? css`
|
||||||
background: ${props.theme.colors[props.color]});
|
background: ${props.theme.colors[props.color]});
|
||||||
@ -74,7 +74,7 @@ const Outline = styled(Base)<{ invert: boolean }>`
|
|||||||
const Flat = styled(Base)`
|
const Flat = styled(Base)`
|
||||||
background: transparent;
|
background: transparent;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors[props.color], 0.2)};
|
background: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ const LineX = styled.span<{ color: string }>`
|
|||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%);
|
transform: translate(-50%);
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors[props.color], 1)};
|
background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LineDown = styled(Base)`
|
const LineDown = styled(Base)`
|
||||||
@ -96,7 +96,7 @@ const LineDown = styled(Base)`
|
|||||||
border-width: 0;
|
border-width: 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-bottom-width: 2px;
|
border-bottom-width: 2px;
|
||||||
border-color: ${(props) => mixin.rgba(props.theme.colors[props.color], 0.2)};
|
border-color: ${props => mixin.rgba(props.theme.colors[props.color], 0.2)};
|
||||||
|
|
||||||
&:hover ${LineX} {
|
&:hover ${LineX} {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -109,14 +109,17 @@ const LineDown = styled(Base)`
|
|||||||
const Gradient = styled(Base)`
|
const Gradient = styled(Base)`
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
30deg,
|
30deg,
|
||||||
${(props) => mixin.rgba(props.theme.colors[props.color], 1)},
|
${props => mixin.rgba(props.theme.colors[props.color], 1)},
|
||||||
${(props) => mixin.rgba(props.theme.colors[props.color], 0.5)}
|
${props => mixin.rgba(props.theme.colors[props.color], 0.5)}
|
||||||
);
|
);
|
||||||
text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3);
|
text-shadow: 1px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Relief = styled(Base)`
|
const Relief = styled(Base)`
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors[props.color], 1)};
|
background: ${props => mixin.rgba(props.theme.colors[props.color], 1)};
|
||||||
-webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset;
|
-webkit-box-shadow: 0 -3px 0 0 rgba(0, 0, 0, 0.2) inset;
|
||||||
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2);
|
box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
@ -1,43 +1,32 @@
|
|||||||
import styled, { css, keyframes } from 'styled-components';
|
import styled, { css, keyframes } from 'styled-components';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
import TextareaAutosize from 'react-autosize-textarea';
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
import { CheckCircle, CheckSquareOutline, Clock, Bubble } from 'shared/icons';
|
import { CheckCircle, CheckSquareOutline, Clock } from 'shared/icons';
|
||||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
|
|
||||||
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
|
export const CardMember = styled(TaskAssignee)<{ zIndex: number }>`
|
||||||
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.bg.secondary},
|
box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.secondary},
|
||||||
inset 0 0 0 1px ${(props) => mixin.rgba(props.theme.colors.bg.secondary, 0.07)};
|
inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.secondary, 0.07)};
|
||||||
z-index: ${(props) => props.zIndex};
|
z-index: ${props => props.zIndex};
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentsIcon = styled(Bubble)<{ color: 'success' | 'normal' }>`
|
|
||||||
${(props) =>
|
|
||||||
props.color === 'success' &&
|
|
||||||
css`
|
|
||||||
fill: ${props.theme.colors.success};
|
|
||||||
stroke: ${props.theme.colors.success};
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
|
export const ChecklistIcon = styled(CheckSquareOutline)<{ color: 'success' | 'normal' }>`
|
||||||
${(props) =>
|
${props =>
|
||||||
props.color === 'success' &&
|
props.color === 'success' &&
|
||||||
css`
|
css`
|
||||||
fill: ${props.theme.colors.success};
|
fill: ${props.theme.colors.success};
|
||||||
stroke: ${props.theme.colors.success};
|
stroke: ${props.theme.colors.success};
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ClockIcon = styled(Clock)<{ color: string }>`
|
export const ClockIcon = styled(Clock)<{ color: string }>`
|
||||||
fill: ${(props) => props.color};
|
fill: ${props => props.color};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EditorTextarea = styled(TextareaAutosize)`
|
export const EditorTextarea = styled(TextareaAutosize)`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
resize: none;
|
resize: none;
|
||||||
height: 54px;
|
height: 90px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
background: none;
|
background: none;
|
||||||
@ -49,7 +38,7 @@ export const EditorTextarea = styled(TextareaAutosize)`
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
&:focus {
|
&:focus {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -63,22 +52,6 @@ export const ListCardBadges = styled.div`
|
|||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentsBadge = styled.div`
|
|
||||||
color: #5e6c84;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 6px 4px 0;
|
|
||||||
font-size: 12px;
|
|
||||||
max-width: 100%;
|
|
||||||
min-height: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
padding: 2px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
vertical-align: top;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ListCardBadge = styled.div`
|
export const ListCardBadge = styled.div`
|
||||||
color: #5e6c84;
|
color: #5e6c84;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -101,7 +74,7 @@ export const DescriptionBadge = styled(ListCardBadge)`
|
|||||||
|
|
||||||
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
|
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
${(props) =>
|
${props =>
|
||||||
props.isPastDue &&
|
props.isPastDue &&
|
||||||
css`
|
css`
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
@ -116,7 +89,7 @@ export const ListCardBadgeText = styled.span<{ color?: 'success' | 'normal' }>`
|
|||||||
padding: 0 4px 0 6px;
|
padding: 0 4px 0 6px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
${(props) => props.color === 'success' && `color: ${props.theme.colors.success};`}
|
${props => props.color === 'success' && `color: ${props.theme.colors.success};`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
|
export const ListCardContainer = styled.div<{ isActive: boolean; editable: boolean }>`
|
||||||
@ -127,7 +100,7 @@ export const ListCardContainer = styled.div<{ isActive: boolean; editable: boole
|
|||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
background-color: ${(props) =>
|
background-color: ${props =>
|
||||||
props.isActive && !props.editable
|
props.isActive && !props.editable
|
||||||
? mixin.darken(props.theme.colors.bg.secondary, 0.1)
|
? mixin.darken(props.theme.colors.bg.secondary, 0.1)
|
||||||
: `${props.theme.colors.bg.secondary}`};
|
: `${props.theme.colors.bg.secondary}`};
|
||||||
@ -144,7 +117,7 @@ export const ListCardDetails = styled.div<{ complete: boolean }>`
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
${(props) => props.complete && 'opacity: 0.6;'}
|
${props => props.complete && 'opacity: 0.6;'}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const labelVariantExpandAnimation = keyframes`
|
const labelVariantExpandAnimation = keyframes`
|
||||||
@ -182,7 +155,7 @@ export const ListCardLabelsWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
||||||
${(props) =>
|
${props =>
|
||||||
props.variant === 'small'
|
props.variant === 'small'
|
||||||
? css`
|
? css`
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@ -208,14 +181,14 @@ export const ListCardLabel = styled.span<{ variant: 'small' | 'large' }>`
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: ${(props) => props.color};
|
background-color: ${props => props.color};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirection: 'expand' | 'shrink' }>`
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
${(props) =>
|
${props =>
|
||||||
props.toggleLabels &&
|
props.toggleLabels &&
|
||||||
props.toggleDirection === 'expand' &&
|
props.toggleDirection === 'expand' &&
|
||||||
css`
|
css`
|
||||||
@ -226,7 +199,7 @@ export const ListCardLabels = styled.div<{ toggleLabels: boolean; toggleDirectio
|
|||||||
animation: ${labelTextVariantExpandAnimation} 0.45s ease-out;
|
animation: ${labelTextVariantExpandAnimation} 0.45s ease-out;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
${(props) =>
|
${props =>
|
||||||
props.toggleLabels &&
|
props.toggleLabels &&
|
||||||
props.toggleDirection === 'shrink' &&
|
props.toggleDirection === 'shrink' &&
|
||||||
css`
|
css`
|
||||||
@ -250,7 +223,7 @@ export const ListCardOperation = styled.span`
|
|||||||
top: 2px;
|
top: 2px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.25)};
|
background-color: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.25)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -259,7 +232,7 @@ export const CardTitle = styled.div`
|
|||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
display: block;
|
display: block;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
@ -276,7 +249,7 @@ export const CardMembers = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CompleteIcon = styled(CheckCircle)`
|
export const CompleteIcon = styled(CheckCircle)`
|
||||||
fill: ${(props) => props.theme.colors.success};
|
fill: ${props => props.theme.colors.success};
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
|
@ -23,8 +23,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
CardMembers,
|
CardMembers,
|
||||||
CardTitleText,
|
CardTitleText,
|
||||||
CommentsIcon,
|
|
||||||
CommentsBadge,
|
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
|
||||||
type DueDate = {
|
type DueDate = {
|
||||||
@ -49,7 +47,6 @@ type Props = {
|
|||||||
dueDate?: DueDate;
|
dueDate?: DueDate;
|
||||||
checklists?: Checklist | null;
|
checklists?: Checklist | null;
|
||||||
labels?: Array<ProjectLabel>;
|
labels?: Array<ProjectLabel>;
|
||||||
comments?: { unread: boolean; total: number } | null;
|
|
||||||
watched?: boolean;
|
watched?: boolean;
|
||||||
wrapperProps?: any;
|
wrapperProps?: any;
|
||||||
members?: Array<TaskUser> | null;
|
members?: Array<TaskUser> | null;
|
||||||
@ -61,21 +58,18 @@ type Props = {
|
|||||||
onCardTitleChange?: (name: string) => void;
|
onCardTitleChange?: (name: string) => void;
|
||||||
labelVariant?: CardLabelVariant;
|
labelVariant?: CardLabelVariant;
|
||||||
toggleLabels?: boolean;
|
toggleLabels?: boolean;
|
||||||
isPublic?: boolean;
|
|
||||||
toggleDirection?: 'shrink' | 'expand';
|
toggleDirection?: 'shrink' | 'expand';
|
||||||
};
|
};
|
||||||
|
|
||||||
const Card = React.forwardRef(
|
const Card = React.forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
isPublic = false,
|
|
||||||
wrapperProps,
|
wrapperProps,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
taskID,
|
taskID,
|
||||||
taskGroupID,
|
taskGroupID,
|
||||||
complete,
|
complete,
|
||||||
toggleLabels = false,
|
toggleLabels = false,
|
||||||
comments,
|
|
||||||
toggleDirection = 'shrink',
|
toggleDirection = 'shrink',
|
||||||
setToggleLabels,
|
setToggleLabels,
|
||||||
onClick,
|
onClick,
|
||||||
@ -126,11 +120,9 @@ const Card = React.forwardRef(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onTaskContext = (e: React.MouseEvent) => {
|
const onTaskContext = (e: React.MouseEvent) => {
|
||||||
if (!isPublic) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onOpenComposer();
|
onOpenComposer();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const onOperationClick = (e: React.MouseEvent<HTMLOrSVGElement>) => {
|
const onOperationClick = (e: React.MouseEvent<HTMLOrSVGElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -142,7 +134,7 @@ const Card = React.forwardRef(
|
|||||||
onMouseEnter={() => setActive(true)}
|
onMouseEnter={() => setActive(true)}
|
||||||
onMouseLeave={() => setActive(false)}
|
onMouseLeave={() => setActive(false)}
|
||||||
ref={$cardRef}
|
ref={$cardRef}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e);
|
onClick(e);
|
||||||
}
|
}
|
||||||
@ -153,9 +145,9 @@ const Card = React.forwardRef(
|
|||||||
{...wrapperProps}
|
{...wrapperProps}
|
||||||
>
|
>
|
||||||
<ListCardInnerContainer ref={$innerCardRef}>
|
<ListCardInnerContainer ref={$innerCardRef}>
|
||||||
{!isPublic && isActive && !editable && (
|
{isActive && !editable && (
|
||||||
<ListCardOperation
|
<ListCardOperation
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onContextMenu) {
|
if (onContextMenu) {
|
||||||
onContextMenu($innerCardRef, taskID, taskGroupID);
|
onContextMenu($innerCardRef, taskID, taskGroupID);
|
||||||
@ -171,7 +163,7 @@ const Card = React.forwardRef(
|
|||||||
<ListCardLabels
|
<ListCardLabels
|
||||||
toggleLabels={toggleLabels}
|
toggleLabels={toggleLabels}
|
||||||
toggleDirection={toggleDirection}
|
toggleDirection={toggleDirection}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onCardLabelClick) {
|
if (onCardLabelClick) {
|
||||||
onCardLabelClick();
|
onCardLabelClick();
|
||||||
@ -181,7 +173,7 @@ const Card = React.forwardRef(
|
|||||||
{labels
|
{labels
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.labelColor.position - b.labelColor.position)
|
.sort((a, b) => a.labelColor.position - b.labelColor.position)
|
||||||
.map((label) => (
|
.map(label => (
|
||||||
<ListCardLabel
|
<ListCardLabel
|
||||||
onAnimationEnd={() => {
|
onAnimationEnd={() => {
|
||||||
if (setToggleLabels) {
|
if (setToggleLabels) {
|
||||||
@ -202,13 +194,13 @@ const Card = React.forwardRef(
|
|||||||
<EditorContent>
|
<EditorContent>
|
||||||
{complete && <CompleteIcon width={16} height={16} />}
|
{complete && <CompleteIcon width={16} height={16} />}
|
||||||
<EditorTextarea
|
<EditorTextarea
|
||||||
onChange={(e) => {
|
onChange={e => {
|
||||||
setCardTitle(e.currentTarget.value);
|
setCardTitle(e.currentTarget.value);
|
||||||
if (onCardTitleChange) {
|
if (onCardTitleChange) {
|
||||||
onCardTitleChange(e.currentTarget.value);
|
onCardTitleChange(e.currentTarget.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@ -225,7 +217,7 @@ const Card = React.forwardRef(
|
|||||||
<ListCardBadges>
|
<ListCardBadges>
|
||||||
{watched && (
|
{watched && (
|
||||||
<ListCardBadge>
|
<ListCardBadge>
|
||||||
<Eye width={12} height={12} />
|
<Eye width={8} height={8} />
|
||||||
</ListCardBadge>
|
</ListCardBadge>
|
||||||
)}
|
)}
|
||||||
{dueDate && (
|
{dueDate && (
|
||||||
@ -239,12 +231,6 @@ const Card = React.forwardRef(
|
|||||||
<List width={8} height={8} />
|
<List width={8} height={8} />
|
||||||
</DescriptionBadge>
|
</DescriptionBadge>
|
||||||
)}
|
)}
|
||||||
{comments && (
|
|
||||||
<CommentsBadge>
|
|
||||||
<CommentsIcon color={comments.unread ? 'success' : 'normal'} width={8} height={8} />
|
|
||||||
<ListCardBadgeText color={comments.unread ? 'success' : 'normal'}>{comments.total}</ListCardBadgeText>
|
|
||||||
</CommentsBadge>
|
|
||||||
)}
|
|
||||||
{checklists && (
|
{checklists && (
|
||||||
<ListCardBadge>
|
<ListCardBadge>
|
||||||
<ChecklistIcon
|
<ChecklistIcon
|
||||||
@ -266,7 +252,7 @@ const Card = React.forwardRef(
|
|||||||
size={28}
|
size={28}
|
||||||
zIndex={members.length - idx}
|
zIndex={members.length - idx}
|
||||||
member={member}
|
member={member}
|
||||||
onMemberProfile={($target) => {
|
onMemberProfile={$target => {
|
||||||
if (onCardMemberClick) {
|
if (onCardMemberClick) {
|
||||||
onCardMemberClick($target, taskID, member.id);
|
onCardMemberClick($target, taskID, member.id);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,14 @@ import {
|
|||||||
SubTitle,
|
SubTitle,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
|
||||||
const Confirm = ({ hasFailed, hasConfirmToken }: ConfirmProps) => {
|
const Confirm = ({ onConfirmUser, hasConfirmToken }: ConfirmProps) => {
|
||||||
|
const [hasFailed, setFailed] = useState(false);
|
||||||
|
const setHasFailed = () => {
|
||||||
|
setFailed(true);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
onConfirmUser(setHasFailed);
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Column>
|
<Column>
|
||||||
|
@ -16,7 +16,7 @@ const InputWrapper = styled.div<{ width: string }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const InputLabel = styled.span<{ width: string }>`
|
const InputLabel = styled.span<{ width: string }>`
|
||||||
width: ${(props) => props.width};
|
width: ${props => props.width};
|
||||||
padding: 0.7rem !important;
|
padding: 0.7rem !important;
|
||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -40,13 +40,13 @@ const InputInput = styled.input<{
|
|||||||
focusBg: string;
|
focusBg: string;
|
||||||
borderColor: string;
|
borderColor: string;
|
||||||
}>`
|
}>`
|
||||||
width: ${(props) => props.width};
|
width: ${props => props.width};
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
border-color: ${(props) => props.borderColor};
|
border-color: ${props => props.borderColor};
|
||||||
background: #262c49;
|
background: #262c49;
|
||||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
|
||||||
${(props) => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
|
${props => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -55,13 +55,13 @@ const InputInput = styled.input<{
|
|||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
|
||||||
border: 1px solid rgba(115, 103, 240);
|
border: 1px solid rgba(115, 103, 240);
|
||||||
background: ${(props) => props.focusBg};
|
background: ${props => props.focusBg};
|
||||||
}
|
}
|
||||||
&:focus ~ ${InputLabel} {
|
&:focus ~ ${InputLabel} {
|
||||||
color: ${(props) => props.theme.colors.primary};
|
color: ${props => props.theme.colors.primary};
|
||||||
transform: translate(-3px, -90%);
|
transform: translate(-3px, -90%);
|
||||||
}
|
}
|
||||||
${(props) =>
|
${props =>
|
||||||
props.hasValue &&
|
props.hasValue &&
|
||||||
css`
|
css`
|
||||||
& ~ ${InputLabel} {
|
& ~ ${InputLabel} {
|
||||||
@ -94,13 +94,11 @@ type ControlledInputProps = {
|
|||||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
value?: string;
|
value?: string;
|
||||||
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
|
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
|
||||||
disabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ControlledInput = ({
|
const ControlledInput = ({
|
||||||
width = 'auto',
|
width = 'auto',
|
||||||
variant = 'normal',
|
variant = 'normal',
|
||||||
disabled = false,
|
|
||||||
type = 'text',
|
type = 'text',
|
||||||
autocomplete,
|
autocomplete,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
@ -128,9 +126,8 @@ const ControlledInput = ({
|
|||||||
return (
|
return (
|
||||||
<InputWrapper className={className} width={width}>
|
<InputWrapper className={className} width={width}>
|
||||||
<InputInput
|
<InputInput
|
||||||
disabled={disabled}
|
|
||||||
hasValue={hasValue}
|
hasValue={hasValue}
|
||||||
onChange={(e) => {
|
onChange={e => {
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
setHasValue(e.currentTarget.value !== '' || floatingLabel);
|
setHasValue(e.currentTarget.value !== '' || floatingLabel);
|
||||||
onChange(e);
|
onChange(e);
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
|
import Input from 'shared/components/Input';
|
||||||
import ControlledInput from 'shared/components/ControlledInput';
|
import ControlledInput from 'shared/components/ControlledInput';
|
||||||
import { Bell, Clock } from 'shared/icons';
|
import { Clock } from 'shared/icons';
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
display: flex
|
display: flex
|
||||||
@ -21,27 +22,27 @@ display: flex
|
|||||||
& .react-datepicker__close-icon::after {
|
& .react-datepicker__close-icon::after {
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
}
|
}
|
||||||
|
|
||||||
& .react-datepicker-time__header {
|
& .react-datepicker-time__header {
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
}
|
}
|
||||||
& .react-datepicker__time-list-item {
|
& .react-datepicker__time-list-item {
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
}
|
}
|
||||||
& .react-datepicker__time-container .react-datepicker__time
|
& .react-datepicker__time-container .react-datepicker__time
|
||||||
.react-datepicker__time-box ul.react-datepicker__time-list
|
.react-datepicker__time-box ul.react-datepicker__time-list
|
||||||
li.react-datepicker__time-list-item:hover {
|
li.react-datepicker__time-list-item:hover {
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
background: ${(props) => props.theme.colors.bg.secondary};
|
background: ${props => props.theme.colors.bg.secondary};
|
||||||
}
|
}
|
||||||
& .react-datepicker__time-container .react-datepicker__time {
|
& .react-datepicker__time-container .react-datepicker__time {
|
||||||
background: ${(props) => props.theme.colors.bg.primary};
|
background: ${props => props.theme.colors.bg.primary};
|
||||||
}
|
}
|
||||||
& .react-datepicker--time-only {
|
& .react-datepicker--time-only {
|
||||||
background: ${(props) => props.theme.colors.bg.primary};
|
background: ${props => props.theme.colors.bg.primary};
|
||||||
border: 1px solid ${(props) => props.theme.colors.border};
|
border: 1px solid ${props => props.theme.colors.border};
|
||||||
}
|
}
|
||||||
|
|
||||||
& .react-datepicker * {
|
& .react-datepicker * {
|
||||||
@ -81,12 +82,12 @@ display: flex
|
|||||||
}
|
}
|
||||||
& .react-datepicker__day--selected {
|
& .react-datepicker__day--selected {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
& .react-datepicker__day--selected:hover {
|
& .react-datepicker__day--selected:hover {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
& .react-datepicker__header {
|
& .react-datepicker__header {
|
||||||
@ -94,12 +95,12 @@ display: flex
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
& .react-datepicker__header--time {
|
& .react-datepicker__header--time {
|
||||||
border-bottom: 1px solid ${(props) => props.theme.colors.border};
|
border-bottom: 1px solid ${props => props.theme.colors.border};
|
||||||
}
|
}
|
||||||
|
|
||||||
& .react-datepicker__input-container input {
|
& .react-datepicker__input-container input {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
border-color: ${(props) => props.theme.colors.alternate};
|
border-color: ${props => props.theme.colors.alternate};
|
||||||
background: #262c49;
|
background: #262c49;
|
||||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
|
||||||
padding: 0.7rem;
|
padding: 0.7rem;
|
||||||
@ -113,7 +114,7 @@ padding: 0.7rem;
|
|||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
|
||||||
border: 1px solid rgba(115, 103, 240);
|
border: 1px solid rgba(115, 103, 240);
|
||||||
background: ${(props) => props.theme.colors.bg.primary};
|
background: ${props => props.theme.colors.bg.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -141,9 +142,9 @@ export const AddDateRange = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.primary, 0.8)};
|
color: ${props => mixin.rgba(props.theme.colors.primary, 0.8)};
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.primary, 1)};
|
color: ${props => mixin.rgba(props.theme.colors.primary, 1)};
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -200,62 +201,18 @@ export const ActionsWrapper = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
& .react-datepicker-wrapper {
|
& .react-datepicker-wrapper {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 86px;
|
width: 82px;
|
||||||
}
|
}
|
||||||
& .react-datepicker__input-container input {
|
& .react-datepicker__input-container input {
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .react-period-select__indicators {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
& .react-period {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 86px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .react-period-select__single-value {
|
|
||||||
color: #c2c6dc;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
& .react-period-select__value-container {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
& .react-period-select__control {
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
min-height: 30px;
|
|
||||||
border-color: rgb(65, 69, 97);
|
|
||||||
background: #262c49;
|
|
||||||
box-shadow: 0 0 0 0 rgb(0 0 0 / 15%);
|
|
||||||
color: #c2c6dc;
|
|
||||||
padding-right: 12px;
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
padding-top: 4px;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActionClock = styled(Clock)`
|
export const ActionClock = styled(Clock)`
|
||||||
align-self: center;
|
align-self: center;
|
||||||
fill: ${(props) => props.theme.colors.primary};
|
fill: ${props => props.theme.colors.primary};
|
||||||
margin: 0 8px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ActionBell = styled(Bell)`
|
|
||||||
align-self: center;
|
|
||||||
fill: ${(props) => props.theme.colors.primary};
|
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
`;
|
`;
|
||||||
@ -265,7 +222,7 @@ export const ActionLabel = styled.div`
|
|||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActionIcon = styled.div<{ disabled?: boolean }>`
|
export const ActionIcon = styled.div`
|
||||||
height: 36px;
|
height: 36px;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
@ -275,25 +232,17 @@ export const ActionIcon = styled.div<{ disabled?: boolean }>`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => props.theme.colors.text.primary};
|
fill: ${props => props.theme.colors.text.primary};
|
||||||
transition-duration: 0.2s;
|
transition-duration: 0.2s;
|
||||||
transition-property: background, border, box-shadow, fill;
|
transition-property: background, border, box-shadow, fill;
|
||||||
}
|
}
|
||||||
&:hover svg {
|
&:hover svg {
|
||||||
fill: ${(props) => props.theme.colors.text.secondary};
|
fill: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.disabled &&
|
|
||||||
css`
|
|
||||||
opacity: 0.8;
|
|
||||||
cursor: not-allowed;
|
|
||||||
`}
|
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ClearButton = styled.div`
|
export const ClearButton = styled.div`
|
||||||
@ -311,38 +260,8 @@ export const ClearButton = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition-duration: 0.2s;
|
transition-duration: 0.2s;
|
||||||
transition-property: background, border, box-shadow, color, fill;
|
transition-property: background, border, box-shadow, color, fill;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ControlWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const RightWrapper = styled.div`
|
|
||||||
flex: 1 1 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const LeftWrapper = styled.div`
|
|
||||||
flex: 1 1 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SaveButton = styled(Button)`
|
|
||||||
padding: 6px 12px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const RemoveButton = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
@ -3,21 +3,14 @@ import dayjs from 'dayjs';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { colourStyles } from 'shared/components/Select';
|
|
||||||
import produce from 'immer';
|
|
||||||
import Select from 'react-select';
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import { getYear, getMonth } from 'date-fns';
|
import { getYear, getMonth } from 'date-fns';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import NOOP from 'shared/utils/noop';
|
import NOOP from 'shared/utils/noop';
|
||||||
import { Bell, Clock, Cross, Plus, Trash } from 'shared/icons';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
RemoveDueDate,
|
RemoveDueDate,
|
||||||
SaveButton,
|
|
||||||
RightWrapper,
|
|
||||||
LeftWrapper,
|
|
||||||
DueDateInput,
|
DueDateInput,
|
||||||
DueDatePickerWrapper,
|
DueDatePickerWrapper,
|
||||||
ConfirmAddDueDate,
|
ConfirmAddDueDate,
|
||||||
@ -29,19 +22,13 @@ import {
|
|||||||
ActionsSeparator,
|
ActionsSeparator,
|
||||||
ActionClock,
|
ActionClock,
|
||||||
ActionLabel,
|
ActionLabel,
|
||||||
ControlWrapper,
|
|
||||||
RemoveButton,
|
|
||||||
ActionBell,
|
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
import { Clock, Cross } from 'shared/icons';
|
||||||
|
import Select from 'react-select/src/Select';
|
||||||
|
|
||||||
type DueDateManagerProps = {
|
type DueDateManagerProps = {
|
||||||
task: Task;
|
task: Task;
|
||||||
onDueDateChange: (
|
onDueDateChange: (task: Task, newDueDate: Date, hasTime: boolean) => void;
|
||||||
task: Task,
|
|
||||||
newDueDate: Date,
|
|
||||||
hasTime: boolean,
|
|
||||||
notifications: { current: Array<NotificationInternal>; removed: Array<string> },
|
|
||||||
) => void;
|
|
||||||
onRemoveDueDate: (task: Task) => void;
|
onRemoveDueDate: (task: Task) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
@ -54,39 +41,6 @@ const FormField = styled.div`
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NotificationCount = styled.input``;
|
|
||||||
|
|
||||||
const ActionPlus = styled(Plus)`
|
|
||||||
position: absolute;
|
|
||||||
fill: ${(props) => props.theme.colors.bg.primary} !important;
|
|
||||||
stroke: ${(props) => props.theme.colors.bg.primary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ActionInput = styled.input`
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 4px;
|
|
||||||
border-color: rgb(65, 69, 97);
|
|
||||||
background: #262c49;
|
|
||||||
box-shadow: 0 0 0 0 rgb(0 0 0 / 15%);
|
|
||||||
color: #c2c6dc;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
padding: 0 12px;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
padding-top: 4px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 48px;
|
|
||||||
&::-webkit-outer-spin-button,
|
|
||||||
&::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const HeaderSelectLabel = styled.div`
|
const HeaderSelectLabel = styled.div`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -105,7 +59,7 @@ const HeaderSelectLabel = styled.div`
|
|||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -124,12 +78,12 @@ const HeaderSelect = styled.select`
|
|||||||
|
|
||||||
& option {
|
& option {
|
||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
background: ${(props) => props.theme.colors.bg.primary};
|
background: ${props => props.theme.colors.bg.primary};
|
||||||
}
|
}
|
||||||
|
|
||||||
& option:hover {
|
& option:hover {
|
||||||
background: ${(props) => props.theme.colors.bg.secondary};
|
background: ${props => props.theme.colors.bg.secondary};
|
||||||
border: 1px solid ${(props) => props.theme.colors.primary};
|
border: 1px solid ${props => props.theme.colors.primary};
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
@ -161,7 +115,7 @@ const HeaderButton = styled.button`
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -177,82 +131,35 @@ const HeaderActions = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const notificationPeriodOptions = [
|
|
||||||
{ value: 'minute', label: 'Minutes' },
|
|
||||||
{ value: 'hour', label: 'Hours' },
|
|
||||||
{ value: 'day', label: 'Days' },
|
|
||||||
{ value: 'week', label: 'Weeks' },
|
|
||||||
];
|
|
||||||
|
|
||||||
type NotificationInternal = {
|
|
||||||
internalId: string;
|
|
||||||
externalId: string | null;
|
|
||||||
period: number;
|
|
||||||
duration: { value: string; label: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
type NotificationEntryProps = {
|
|
||||||
notification: NotificationInternal;
|
|
||||||
onChange: (period: number, duration: { value: string; label: string }) => void;
|
|
||||||
onRemove: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NotificationEntry: React.FC<NotificationEntryProps> = ({ notification, onChange, onRemove }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionBell width={16} height={16} />
|
|
||||||
<ActionLabel>Notification</ActionLabel>
|
|
||||||
<ActionInput
|
|
||||||
value={notification.period}
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(parseInt(e.currentTarget.value, 10), notification.duration);
|
|
||||||
}}
|
|
||||||
onKeyPress={(e) => {
|
|
||||||
const isNumber = /^[0-9]$/i.test(e.key);
|
|
||||||
if (!isNumber && e.key !== 'Backspace') {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
dir="ltr"
|
|
||||||
autoComplete="off"
|
|
||||||
min="0"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
menuPlacement="top"
|
|
||||||
className="react-period"
|
|
||||||
classNamePrefix="react-period-select"
|
|
||||||
styles={colourStyles}
|
|
||||||
isSearchable={false}
|
|
||||||
defaultValue={notification.duration}
|
|
||||||
options={notificationPeriodOptions}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e !== null) {
|
|
||||||
onChange(notification.period, e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ActionIcon onClick={() => onRemove()}>
|
|
||||||
<Cross width={16} height={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
|
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
|
||||||
const currentDueDate = task.dueDate.at ? dayjs(task.dueDate.at).toDate() : null;
|
const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null;
|
||||||
const {
|
const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>();
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
setError,
|
|
||||||
formState: { errors },
|
|
||||||
control,
|
|
||||||
} = useForm<DueDateFormData>();
|
|
||||||
|
|
||||||
const [startDate, setStartDate] = useState<Date | null>(currentDueDate);
|
const [startDate, setStartDate] = useState<Date | null>(currentDueDate);
|
||||||
const [endDate, setEndDate] = useState<Date | null>(currentDueDate);
|
const [endDate, setEndDate] = useState<Date | null>(currentDueDate);
|
||||||
const [hasTime, enableTime] = useState(task.hasTime ?? false);
|
const [hasTime, enableTime] = useState(task.hasTime ?? false);
|
||||||
|
const firstRun = useRef<boolean>(true);
|
||||||
|
|
||||||
|
const debouncedFunctionRef = useRef((newDate: Date | null, nowHasTime: boolean) => {
|
||||||
|
if (!firstRun.current) {
|
||||||
|
if (newDate) {
|
||||||
|
onDueDateChange(task, newDate, nowHasTime);
|
||||||
|
} else {
|
||||||
|
onRemoveDueDate(task);
|
||||||
|
enableTime(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
firstRun.current = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const debouncedChange = useCallback(
|
||||||
|
_.debounce((newDate, nowHasTime) => debouncedFunctionRef.current(newDate, nowHasTime), 500),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedChange(startDate, hasTime);
|
||||||
|
}, [startDate, hasTime]);
|
||||||
const years = _.range(2010, getYear(new Date()) + 10, 1);
|
const years = _.range(2010, getYear(new Date()) + 10, 1);
|
||||||
const months = [
|
const months = [
|
||||||
'January',
|
'January',
|
||||||
@ -269,46 +176,45 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
'December',
|
'December',
|
||||||
];
|
];
|
||||||
|
|
||||||
const [isRange, setIsRange] = useState(false);
|
const onChange = (dates: any) => {
|
||||||
const [notDuration, setNotDuration] = useState(10);
|
const [start, end] = dates;
|
||||||
const [removedNotifications, setRemovedNotifications] = useState<Array<string>>([]);
|
setStartDate(start);
|
||||||
const [notifications, setNotifications] = useState<Array<NotificationInternal>>(
|
setEndDate(end);
|
||||||
task.dueDate.notifications
|
|
||||||
? task.dueDate.notifications.map((c, idx) => {
|
|
||||||
const duration =
|
|
||||||
notificationPeriodOptions.find((o) => o.value === c.duration.toLowerCase()) ?? notificationPeriodOptions[0];
|
|
||||||
return {
|
|
||||||
internalId: `n${idx}`,
|
|
||||||
externalId: c.id,
|
|
||||||
period: c.period,
|
|
||||||
duration,
|
|
||||||
};
|
};
|
||||||
})
|
const [isRange, setIsRange] = useState(false);
|
||||||
: [],
|
|
||||||
|
const CustomTimeInput = forwardRef(({ value, onClick, onChange, onBlur, onFocus }: any, $ref: any) => {
|
||||||
|
return (
|
||||||
|
<DueDateInput
|
||||||
|
id="endTime"
|
||||||
|
value={value}
|
||||||
|
name="endTime"
|
||||||
|
onChange={onChange}
|
||||||
|
width="100%"
|
||||||
|
variant="alternate"
|
||||||
|
label="Time"
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<DateRangeInputs>
|
<DateRangeInputs>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
selected={startDate}
|
selected={startDate}
|
||||||
onChange={(date) => {
|
onChange={date => setStartDate(date)}
|
||||||
if (!Array.isArray(date) && date !== null) {
|
|
||||||
setStartDate(date);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
popperClassName="picker-hidden"
|
popperClassName="picker-hidden"
|
||||||
dateFormat="yyyy-MM-dd"
|
dateFormat="yyyy-MM-dd"
|
||||||
disabledKeyboardNavigation
|
disabledKeyboardNavigation
|
||||||
|
isClearable
|
||||||
placeholderText="Select due date"
|
placeholderText="Select due date"
|
||||||
/>
|
/>
|
||||||
{isRange ? (
|
{isRange ? (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
selected={startDate}
|
selected={startDate}
|
||||||
onChange={(date) => {
|
isClearable
|
||||||
if (!Array.isArray(date)) {
|
onChange={date => setStartDate(date)}
|
||||||
setStartDate(date);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
popperClassName="picker-hidden"
|
popperClassName="picker-hidden"
|
||||||
dateFormat="yyyy-MM-dd"
|
dateFormat="yyyy-MM-dd"
|
||||||
placeholderText="Select from date"
|
placeholderText="Select from date"
|
||||||
@ -319,11 +225,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
</DateRangeInputs>
|
</DateRangeInputs>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
selected={startDate}
|
selected={startDate}
|
||||||
onChange={(date) => {
|
onChange={date => setStartDate(date)}
|
||||||
if (!Array.isArray(date)) {
|
|
||||||
setStartDate(date);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
useWeekdaysShort
|
useWeekdaysShort
|
||||||
renderCustomHeader={({
|
renderCustomHeader={({
|
||||||
@ -345,7 +247,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
value={months[getMonth(date)]}
|
value={months[getMonth(date)]}
|
||||||
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
|
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
|
||||||
>
|
>
|
||||||
{months.map((option) => (
|
{months.map(option => (
|
||||||
<option key={option} value={option}>
|
<option key={option} value={option}>
|
||||||
{option}
|
{option}
|
||||||
</option>
|
</option>
|
||||||
@ -355,7 +257,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
<HeaderSelectLabel>
|
<HeaderSelectLabel>
|
||||||
{date.getFullYear()}
|
{date.getFullYear()}
|
||||||
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}>
|
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}>
|
||||||
{years.map((option) => (
|
{years.map(option => (
|
||||||
<option key={option} value={option}>
|
<option key={option} value={option}>
|
||||||
{option}
|
{option}
|
||||||
</option>
|
</option>
|
||||||
@ -377,10 +279,8 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
<ActionLabel>Due Time</ActionLabel>
|
<ActionLabel>Due Time</ActionLabel>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
selected={startDate}
|
selected={startDate}
|
||||||
onChange={(date) => {
|
onChange={date => {
|
||||||
if (!Array.isArray(date)) {
|
|
||||||
setStartDate(date);
|
setStartDate(date);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
showTimeSelect
|
showTimeSelect
|
||||||
showTimeSelectOnly
|
showTimeSelectOnly
|
||||||
@ -393,75 +293,7 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</ActionsWrapper>
|
</ActionsWrapper>
|
||||||
)}
|
)}
|
||||||
{notifications.map((n, idx) => (
|
<ActionsWrapper>
|
||||||
<ActionsWrapper key={n.internalId}>
|
|
||||||
<NotificationEntry
|
|
||||||
notification={n}
|
|
||||||
onChange={(period, duration) => {
|
|
||||||
setNotifications((prev) =>
|
|
||||||
produce(prev, (draft) => {
|
|
||||||
draft[idx].duration = duration;
|
|
||||||
draft[idx].period = period;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onRemove={() => {
|
|
||||||
setNotifications((prev) =>
|
|
||||||
produce(prev, (draft) => {
|
|
||||||
draft.splice(idx, 1);
|
|
||||||
if (n.externalId !== null) {
|
|
||||||
setRemovedNotifications((prev) => {
|
|
||||||
if (n.externalId !== null) {
|
|
||||||
return [...prev, n.externalId];
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ActionsWrapper>
|
|
||||||
))}
|
|
||||||
<ControlWrapper>
|
|
||||||
<LeftWrapper>
|
|
||||||
<SaveButton
|
|
||||||
onClick={() => {
|
|
||||||
if (startDate && notifications.findIndex((n) => Number.isNaN(n.period)) === -1) {
|
|
||||||
onDueDateChange(task, startDate, hasTime, { current: notifications, removed: removedNotifications });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</SaveButton>
|
|
||||||
{currentDueDate !== null && (
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => {
|
|
||||||
onRemoveDueDate(task);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash width={16} height={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
</LeftWrapper>
|
|
||||||
<RightWrapper>
|
|
||||||
<ActionIcon
|
|
||||||
disabled={notifications.length === 3}
|
|
||||||
onClick={() => {
|
|
||||||
setNotifications((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
externalId: null,
|
|
||||||
internalId: `n${prev.length + 1}`,
|
|
||||||
duration: notificationPeriodOptions[0],
|
|
||||||
period: 10,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Bell width={16} height={16} />
|
|
||||||
<ActionPlus width={8} height={8} />
|
|
||||||
</ActionIcon>
|
|
||||||
{!hasTime && (
|
{!hasTime && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -476,8 +308,8 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
|
|||||||
<Clock width={16} height={16} />
|
<Clock width={16} height={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
)}
|
||||||
</RightWrapper>
|
<ClearButton onClick={() => setStartDate(null)}>{hasTime ? 'Clear all' : 'Clear'}</ClearButton>
|
||||||
</ControlWrapper>
|
</ActionsWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,202 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import styled, { css } from 'styled-components/macro';
|
|
||||||
import theme from '../../../App/ThemeStyles';
|
|
||||||
|
|
||||||
const InputWrapper = styled.div<{ width: string }>`
|
|
||||||
position: relative;
|
|
||||||
width: ${(props) => props.width};
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
margin-bottom: 2.2rem;
|
|
||||||
margin-top: 24px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InputLabel = styled.span<{ width: string }>`
|
|
||||||
width: ${(props) => props.width};
|
|
||||||
padding: 0.7rem !important;
|
|
||||||
color: #c2c6dc;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
cursor: text;
|
|
||||||
font-size: 12px;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const InputInput = styled.input<{
|
|
||||||
hasValue: boolean;
|
|
||||||
hasIcon: boolean;
|
|
||||||
width: string;
|
|
||||||
focusBg: string;
|
|
||||||
borderColor: string;
|
|
||||||
}>`
|
|
||||||
width: ${(props) => props.width};
|
|
||||||
font-size: 14px;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
border-color: ${(props) => props.borderColor};
|
|
||||||
background: #262c49;
|
|
||||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
|
|
||||||
${(props) => (props.hasIcon ? 'padding: 0.7rem 1rem 0.7rem 3rem;' : 'padding: 0.7rem;')}
|
|
||||||
|
|
||||||
&:-webkit-autofill, &:-webkit-autofill:hover, &:-webkit-autofill:focus, &:-webkit-autofill:active {
|
|
||||||
-webkit-box-shadow: 0 0 0 30px #262c49 inset !important;
|
|
||||||
}
|
|
||||||
&:-webkit-autofill {
|
|
||||||
-webkit-text-fill-color: #c2c6dc !important;
|
|
||||||
}
|
|
||||||
line-height: 16px;
|
|
||||||
color: #c2c6dc;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
|
|
||||||
border: 1px solid ${(props) => props.theme.colors.primary};
|
|
||||||
background: ${(props) => props.focusBg};
|
|
||||||
}
|
|
||||||
&:focus ~ ${InputLabel} {
|
|
||||||
color: ${(props) => props.theme.colors.primary};
|
|
||||||
transform: translate(-3px, -90%);
|
|
||||||
}
|
|
||||||
${(props) =>
|
|
||||||
props.hasValue &&
|
|
||||||
css`
|
|
||||||
& ~ ${InputLabel} {
|
|
||||||
color: ${props.theme.colors.primary};
|
|
||||||
transform: translate(-3px, -90%);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Icon = styled.div`
|
|
||||||
display: flex;
|
|
||||||
left: 16px;
|
|
||||||
position: absolute;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type FormInputProps = {
|
|
||||||
variant?: 'normal' | 'alternate';
|
|
||||||
disabled?: boolean;
|
|
||||||
label?: string;
|
|
||||||
width?: string;
|
|
||||||
floatingLabel?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
icon?: JSX.Element;
|
|
||||||
type?: string;
|
|
||||||
autocomplete?: boolean;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
autoSelect?: boolean;
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
onChange: any;
|
|
||||||
onBlur: any;
|
|
||||||
className?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
value?: string;
|
|
||||||
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function useCombinedRefs(...refs: any) {
|
|
||||||
const targetRef = React.useRef();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
refs.forEach((ref: any) => {
|
|
||||||
if (!ref) return;
|
|
||||||
|
|
||||||
if (typeof ref === 'function') {
|
|
||||||
ref(targetRef.current);
|
|
||||||
} else {
|
|
||||||
ref.current = targetRef.current;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [refs]);
|
|
||||||
|
|
||||||
return targetRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormInput = React.forwardRef(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
disabled = false,
|
|
||||||
width = 'auto',
|
|
||||||
variant = 'normal',
|
|
||||||
type = 'text',
|
|
||||||
autoFocus = false,
|
|
||||||
autoSelect = false,
|
|
||||||
autocomplete,
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
onBlur,
|
|
||||||
onChange,
|
|
||||||
icon,
|
|
||||||
name,
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
floatingLabel,
|
|
||||||
defaultValue,
|
|
||||||
value,
|
|
||||||
id,
|
|
||||||
}: FormInputProps,
|
|
||||||
$ref: any,
|
|
||||||
) => {
|
|
||||||
const [hasValue, setHasValue] = useState(defaultValue !== '');
|
|
||||||
const borderColor = variant === 'normal' ? 'rgba(0,0,0,0.2)' : theme.colors.alternate;
|
|
||||||
const focusBg = variant === 'normal' ? theme.colors.bg.secondary : theme.colors.bg.primary;
|
|
||||||
|
|
||||||
// Merge forwarded ref and internal ref in order to be able to access the ref in the useEffect
|
|
||||||
// The forwarded ref is not accessible by itself, which is what the innerRef & combined ref is for
|
|
||||||
// TODO(jordanknott): This is super ugly, find a better approach?
|
|
||||||
const $innerRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
const combinedRef: any = useCombinedRefs($ref, $innerRef);
|
|
||||||
useEffect(() => {
|
|
||||||
if (combinedRef && combinedRef.current) {
|
|
||||||
if (autoFocus) {
|
|
||||||
combinedRef.current.focus();
|
|
||||||
}
|
|
||||||
if (autoSelect) {
|
|
||||||
combinedRef.current.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<InputWrapper className={className} width={width}>
|
|
||||||
<InputInput
|
|
||||||
onChange={(e) => {
|
|
||||||
setHasValue((e.currentTarget.value !== '' || floatingLabel) ?? false);
|
|
||||||
onChange(e);
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
hasValue={hasValue}
|
|
||||||
ref={combinedRef}
|
|
||||||
id={id}
|
|
||||||
type={type}
|
|
||||||
name={name}
|
|
||||||
onClick={onClick}
|
|
||||||
autoComplete={autocomplete ? 'on' : 'off'}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
onBlur={onBlur}
|
|
||||||
value={value}
|
|
||||||
hasIcon={typeof icon !== 'undefined'}
|
|
||||||
width={width}
|
|
||||||
placeholder={placeholder}
|
|
||||||
focusBg={focusBg}
|
|
||||||
borderColor={borderColor}
|
|
||||||
/>
|
|
||||||
{label && <InputLabel width={width}>{label}</InputLabel>}
|
|
||||||
<Icon>{icon && icon}</Icon>
|
|
||||||
</InputWrapper>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export default FormInput;
|
|
@ -72,9 +72,6 @@ export const HeaderName = styled(TextareaAutosize)`
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: -4px 0;
|
margin: -4px 0;
|
||||||
&:disabled {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
word-spacing: normal;
|
word-spacing: normal;
|
||||||
|
@ -24,7 +24,6 @@ type Props = {
|
|||||||
onOpenComposer: (id: string) => void;
|
onOpenComposer: (id: string) => void;
|
||||||
wrapperProps?: any;
|
wrapperProps?: any;
|
||||||
headerProps?: any;
|
headerProps?: any;
|
||||||
isPublic: boolean;
|
|
||||||
index?: number;
|
index?: number;
|
||||||
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
|
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
};
|
};
|
||||||
@ -38,7 +37,6 @@ const List = React.forwardRef(
|
|||||||
isComposerOpen,
|
isComposerOpen,
|
||||||
onOpenComposer,
|
onOpenComposer,
|
||||||
children,
|
children,
|
||||||
isPublic,
|
|
||||||
wrapperProps,
|
wrapperProps,
|
||||||
headerProps,
|
headerProps,
|
||||||
onExtraMenuOpen,
|
onExtraMenuOpen,
|
||||||
@ -88,37 +86,39 @@ const List = React.forwardRef(
|
|||||||
<Container ref={$wrapperRef} {...wrapperProps}>
|
<Container ref={$wrapperRef} {...wrapperProps}>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Header {...headerProps} isEditing={isEditingTitle}>
|
<Header {...headerProps} isEditing={isEditingTitle}>
|
||||||
{!isPublic && <HeaderEditTarget onClick={onClick} isHidden={isEditingTitle} />}
|
<HeaderEditTarget onClick={onClick} isHidden={isEditingTitle} />
|
||||||
<HeaderName
|
<HeaderName
|
||||||
ref={$listNameRef}
|
ref={$listNameRef}
|
||||||
disabled={isPublic}
|
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
value={listName}
|
value={listName}
|
||||||
/>
|
/>
|
||||||
{!isPublic && (
|
|
||||||
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
|
<ListExtraMenuButtonWrapper ref={$extraActionsRef} onClick={handleExtraMenuOpen}>
|
||||||
<Ellipsis vertical={false} size={16} color="#c2c6dc" />
|
<Ellipsis size={16} color="#c2c6dc" />
|
||||||
</ListExtraMenuButtonWrapper>
|
</ListExtraMenuButtonWrapper>
|
||||||
)}
|
|
||||||
</Header>
|
</Header>
|
||||||
{children && children}
|
{children && children}
|
||||||
{!isPublic && (
|
|
||||||
<AddCardContainer hidden={isComposerOpen}>
|
<AddCardContainer hidden={isComposerOpen}>
|
||||||
<AddCardButton onClick={() => onOpenComposer(id)}>
|
<AddCardButton onClick={() => onOpenComposer(id)}>
|
||||||
<Plus width={12} height={12} />
|
<Plus width={12} height={12} />
|
||||||
<AddCardButtonText>Add another card</AddCardButtonText>
|
<AddCardButtonText>Add another card</AddCardButtonText>
|
||||||
</AddCardButton>
|
</AddCardButton>
|
||||||
</AddCardContainer>
|
</AddCardContainer>
|
||||||
)}
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
List.defaultProps = {
|
||||||
|
children: null,
|
||||||
|
isComposerOpen: false,
|
||||||
|
wrapperProps: {},
|
||||||
|
headerProps: {},
|
||||||
|
};
|
||||||
|
|
||||||
List.displayName = 'List';
|
List.displayName = 'List';
|
||||||
export default List;
|
export default List;
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import List, { ListCards } from 'shared/components/List';
|
|||||||
import Card from 'shared/components/Card';
|
import Card from 'shared/components/Card';
|
||||||
import CardComposer from 'shared/components/CardComposer';
|
import CardComposer from 'shared/components/CardComposer';
|
||||||
import AddList from 'shared/components/AddList';
|
import AddList from 'shared/components/AddList';
|
||||||
import log from 'loglevel';
|
|
||||||
import {
|
import {
|
||||||
isPositionChanged,
|
isPositionChanged,
|
||||||
getSortedDraggables,
|
getSortedDraggables,
|
||||||
@ -112,16 +111,24 @@ function shouldStatusFilter(task: Task, filter: TaskStatusFilter) {
|
|||||||
const TODAY = REFERENCE.clone().startOf('day');
|
const TODAY = REFERENCE.clone().startOf('day');
|
||||||
return completedAt.isSame(TODAY, 'd');
|
return completedAt.isSame(TODAY, 'd');
|
||||||
case TaskSince.YESTERDAY:
|
case TaskSince.YESTERDAY:
|
||||||
const YESTERDAY = REFERENCE.clone().subtract(1, 'day').startOf('day');
|
const YESTERDAY = REFERENCE.clone()
|
||||||
|
.subtract(1, 'day')
|
||||||
|
.startOf('day');
|
||||||
return completedAt.isSameOrAfter(YESTERDAY, 'd');
|
return completedAt.isSameOrAfter(YESTERDAY, 'd');
|
||||||
case TaskSince.ONE_WEEK:
|
case TaskSince.ONE_WEEK:
|
||||||
const ONE_WEEK = REFERENCE.clone().subtract(7, 'day').startOf('day');
|
const ONE_WEEK = REFERENCE.clone()
|
||||||
|
.subtract(7, 'day')
|
||||||
|
.startOf('day');
|
||||||
return completedAt.isSameOrAfter(ONE_WEEK, 'd');
|
return completedAt.isSameOrAfter(ONE_WEEK, 'd');
|
||||||
case TaskSince.TWO_WEEKS:
|
case TaskSince.TWO_WEEKS:
|
||||||
const TWO_WEEKS = REFERENCE.clone().subtract(14, 'day').startOf('day');
|
const TWO_WEEKS = REFERENCE.clone()
|
||||||
|
.subtract(14, 'day')
|
||||||
|
.startOf('day');
|
||||||
return completedAt.isSameOrAfter(TWO_WEEKS, 'd');
|
return completedAt.isSameOrAfter(TWO_WEEKS, 'd');
|
||||||
case TaskSince.THREE_WEEKS:
|
case TaskSince.THREE_WEEKS:
|
||||||
const THREE_WEEKS = REFERENCE.clone().subtract(21, 'day').startOf('day');
|
const THREE_WEEKS = REFERENCE.clone()
|
||||||
|
.subtract(21, 'day')
|
||||||
|
.startOf('day');
|
||||||
return completedAt.isSameOrAfter(THREE_WEEKS, 'd');
|
return completedAt.isSameOrAfter(THREE_WEEKS, 'd');
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
@ -144,7 +151,6 @@ interface SimpleProps {
|
|||||||
onCardMemberClick: OnCardMemberClick;
|
onCardMemberClick: OnCardMemberClick;
|
||||||
onCardLabelClick: () => void;
|
onCardLabelClick: () => void;
|
||||||
cardLabelVariant: CardLabelVariant;
|
cardLabelVariant: CardLabelVariant;
|
||||||
isPublic?: boolean;
|
|
||||||
taskStatusFilter?: TaskStatusFilter;
|
taskStatusFilter?: TaskStatusFilter;
|
||||||
taskMetaFilters?: TaskMetaFilters;
|
taskMetaFilters?: TaskMetaFilters;
|
||||||
taskSorting?: TaskSorting;
|
taskSorting?: TaskSorting;
|
||||||
@ -182,7 +188,6 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
onExtraMenuOpen,
|
onExtraMenuOpen,
|
||||||
onCardMemberClick,
|
onCardMemberClick,
|
||||||
taskStatusFilter = initTaskStatusFilter,
|
taskStatusFilter = initTaskStatusFilter,
|
||||||
isPublic = false,
|
|
||||||
taskMetaFilters = initTaskMetaFilters,
|
taskMetaFilters = initTaskMetaFilters,
|
||||||
taskSorting = initTaskSorting,
|
taskSorting = initTaskSorting,
|
||||||
}) => {
|
}) => {
|
||||||
@ -196,14 +201,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
let beforeDropDraggables: Array<DraggableElement> | null = null;
|
let beforeDropDraggables: Array<DraggableElement> | null = null;
|
||||||
|
|
||||||
if (isList) {
|
if (isList) {
|
||||||
const droppedGroup = taskGroups.find((taskGroup) => taskGroup.id === draggableId);
|
const droppedGroup = taskGroups.find(taskGroup => taskGroup.id === draggableId);
|
||||||
if (droppedGroup) {
|
if (droppedGroup) {
|
||||||
droppedDraggable = {
|
droppedDraggable = {
|
||||||
id: draggableId,
|
id: draggableId,
|
||||||
position: droppedGroup.position,
|
position: droppedGroup.position,
|
||||||
};
|
};
|
||||||
beforeDropDraggables = getSortedDraggables(
|
beforeDropDraggables = getSortedDraggables(
|
||||||
taskGroups.map((taskGroup) => {
|
taskGroups.map(taskGroup => {
|
||||||
return { id: taskGroup.id, position: taskGroup.position };
|
return { id: taskGroup.id, position: taskGroup.position };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -227,13 +232,13 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const curTaskGroup = taskGroups.findIndex(
|
const curTaskGroup = taskGroups.findIndex(
|
||||||
(taskGroup) => taskGroup.tasks.findIndex((task) => task.id === draggableId) !== -1,
|
taskGroup => taskGroup.tasks.findIndex(task => task.id === draggableId) !== -1,
|
||||||
);
|
);
|
||||||
let targetTaskGroup = curTaskGroup;
|
let targetTaskGroup = curTaskGroup;
|
||||||
if (!isSameList) {
|
if (!isSameList) {
|
||||||
targetTaskGroup = taskGroups.findIndex((taskGroup) => taskGroup.id === destination.droppableId);
|
targetTaskGroup = taskGroups.findIndex(taskGroup => taskGroup.id === destination.droppableId);
|
||||||
}
|
}
|
||||||
const droppedTask = taskGroups[curTaskGroup].tasks.find((task) => task.id === draggableId);
|
const droppedTask = taskGroups[curTaskGroup].tasks.find(task => task.id === draggableId);
|
||||||
|
|
||||||
if (droppedTask) {
|
if (droppedTask) {
|
||||||
droppedDraggable = {
|
droppedDraggable = {
|
||||||
@ -241,7 +246,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
position: droppedTask.position,
|
position: droppedTask.position,
|
||||||
};
|
};
|
||||||
beforeDropDraggables = getSortedDraggables(
|
beforeDropDraggables = getSortedDraggables(
|
||||||
taskGroups[targetTaskGroup].tasks.map((task) => {
|
taskGroups[targetTaskGroup].tasks.map(task => {
|
||||||
return { id: task.id, position: task.position };
|
return { id: task.id, position: task.position };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -263,9 +268,6 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
id: destination.droppableId,
|
id: destination.droppableId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
log.debug(
|
|
||||||
`action=move taskId=${droppedTask.id} source=${source.droppableId} dest=${destination.droppableId} oldPos=${droppedTask.position} newPos=${newPosition}`,
|
|
||||||
);
|
|
||||||
onTaskDrop(newTask, droppedTask.taskGroup.id);
|
onTaskDrop(newTask, droppedTask.taskGroup.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -282,7 +284,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
<BoardWrapper>
|
<BoardWrapper>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<Droppable direction="horizontal" type="column" droppableId="root">
|
<Droppable direction="horizontal" type="column" droppableId="root">
|
||||||
{(provided) => (
|
{provided => (
|
||||||
<Container {...provided.droppableProps} ref={provided.innerRef}>
|
<Container {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
{taskGroups
|
{taskGroups
|
||||||
.slice()
|
.slice()
|
||||||
@ -290,15 +292,14 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
.map((taskGroup: TaskGroup, index: number) => {
|
.map((taskGroup: TaskGroup, index: number) => {
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
|
<Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
|
||||||
{(columnDragProvided) => (
|
{columnDragProvided => (
|
||||||
<Droppable type="tasks" droppableId={taskGroup.id}>
|
<Droppable type="tasks" droppableId={taskGroup.id}>
|
||||||
{(columnDropProvided, snapshot) => (
|
{(columnDropProvided, snapshot) => (
|
||||||
<List
|
<List
|
||||||
name={taskGroup.name}
|
name={taskGroup.name}
|
||||||
onOpenComposer={(id) => setCurrentComposer(id)}
|
onOpenComposer={id => setCurrentComposer(id)}
|
||||||
isComposerOpen={currentComposer === taskGroup.id}
|
isComposerOpen={currentComposer === taskGroup.id}
|
||||||
onSaveName={(name) => onChangeTaskGroupName(taskGroup.id, name)}
|
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
|
||||||
isPublic={isPublic}
|
|
||||||
ref={columnDragProvided.innerRef}
|
ref={columnDragProvided.innerRef}
|
||||||
wrapperProps={columnDragProvided.draggableProps}
|
wrapperProps={columnDragProvided.draggableProps}
|
||||||
headerProps={columnDragProvided.dragHandleProps}
|
headerProps={columnDragProvided.dragHandleProps}
|
||||||
@ -310,8 +311,8 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
|
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
|
||||||
{taskGroup.tasks
|
{taskGroup.tasks
|
||||||
.slice()
|
.slice()
|
||||||
.filter((t) => shouldStatusFilter(t, taskStatusFilter))
|
.filter(t => shouldStatusFilter(t, taskStatusFilter))
|
||||||
.filter((t) => shouldMetaFilter(t, taskMetaFilters))
|
.filter(t => shouldMetaFilter(t, taskMetaFilters))
|
||||||
.sort((a: any, b: any) => a.position - b.position)
|
.sort((a: any, b: any) => a.position - b.position)
|
||||||
.sort((a: any, b: any) => sortTasks(a, b, taskSorting))
|
.sort((a: any, b: any) => sortTasks(a, b, taskSorting))
|
||||||
.map((task: Task, taskIndex: any) => {
|
.map((task: Task, taskIndex: any) => {
|
||||||
@ -322,14 +323,12 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
index={taskIndex}
|
index={taskIndex}
|
||||||
isDragDisabled={taskSorting.type !== TaskSortingType.NONE}
|
isDragDisabled={taskSorting.type !== TaskSortingType.NONE}
|
||||||
>
|
>
|
||||||
{(taskProvided) => {
|
{taskProvided => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
toggleDirection={toggleDirection}
|
toggleDirection={toggleDirection}
|
||||||
toggleLabels={toggleLabels}
|
toggleLabels={toggleLabels}
|
||||||
isPublic={isPublic}
|
|
||||||
labelVariant={cardLabelVariant}
|
labelVariant={cardLabelVariant}
|
||||||
watched={task.watched}
|
|
||||||
wrapperProps={{
|
wrapperProps={{
|
||||||
...taskProvided.draggableProps,
|
...taskProvided.draggableProps,
|
||||||
...taskProvided.dragHandleProps,
|
...taskProvided.dragHandleProps,
|
||||||
@ -349,12 +348,12 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
complete={task.complete ?? false}
|
complete={task.complete ?? false}
|
||||||
taskGroupID={taskGroup.id}
|
taskGroupID={taskGroup.id}
|
||||||
description=""
|
description=""
|
||||||
labels={task.labels.map((label) => label.projectLabel)}
|
labels={task.labels.map(label => label.projectLabel)}
|
||||||
dueDate={
|
dueDate={
|
||||||
task.dueDate.at
|
task.dueDate
|
||||||
? {
|
? {
|
||||||
isPastDue: false,
|
isPastDue: false,
|
||||||
formattedDate: dayjs(task.dueDate.at).format('MMM D, YYYY'),
|
formattedDate: dayjs(task.dueDate).format('MMM D, YYYY'),
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@ -364,7 +363,6 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
onTaskClick(task);
|
onTaskClick(task);
|
||||||
}}
|
}}
|
||||||
checklists={task.badges && task.badges.checklist}
|
checklists={task.badges && task.badges.checklist}
|
||||||
comments={task.badges && task.badges.comments}
|
|
||||||
onCardMemberClick={onCardMemberClick}
|
onCardMemberClick={onCardMemberClick}
|
||||||
onContextMenu={onQuickEditorOpen}
|
onContextMenu={onQuickEditorOpen}
|
||||||
/>
|
/>
|
||||||
@ -379,7 +377,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setCurrentComposer('');
|
setCurrentComposer('');
|
||||||
}}
|
}}
|
||||||
onCreateCard={(name) => {
|
onCreateCard={name => {
|
||||||
onCreateTask(taskGroup.id, name);
|
onCreateTask(taskGroup.id, name);
|
||||||
}}
|
}}
|
||||||
isOpen
|
isOpen
|
||||||
@ -398,13 +396,11 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
{!isPublic && (
|
|
||||||
<AddList
|
<AddList
|
||||||
onSave={(listName) => {
|
onSave={listName => {
|
||||||
onCreateTaskGroup(listName);
|
onCreateTaskGroup(listName);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</BoardWrapper>
|
</BoardWrapper>
|
||||||
</BoardContainer>
|
</BoardContainer>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
|||||||
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
|
isFiltered = shouldFilter(!(task.dueDate && task.dueDate !== null));
|
||||||
}
|
}
|
||||||
if (task.dueDate) {
|
if (task.dueDate) {
|
||||||
const taskDueDate = dayjs(task.dueDate.at);
|
const taskDueDate = dayjs(task.dueDate);
|
||||||
const today = dayjs();
|
const today = dayjs();
|
||||||
let start;
|
let start;
|
||||||
let end;
|
let end;
|
||||||
@ -36,31 +36,61 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
|||||||
isFiltered = shouldFilter(taskDueDate.isSame(today, 'day'));
|
isFiltered = shouldFilter(taskDueDate.isSame(today, 'day'));
|
||||||
break;
|
break;
|
||||||
case DueDateFilterType.TOMORROW:
|
case DueDateFilterType.TOMORROW:
|
||||||
isFiltered = shouldFilter(taskDueDate.isBefore(today.clone().add(1, 'day').endOf('day')));
|
isFiltered = shouldFilter(
|
||||||
|
taskDueDate.isBefore(
|
||||||
|
today
|
||||||
|
.clone()
|
||||||
|
.add(1, 'day')
|
||||||
|
.endOf('day'),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case DueDateFilterType.THIS_WEEK:
|
case DueDateFilterType.THIS_WEEK:
|
||||||
start = today.clone().weekday(0).startOf('day');
|
start = today
|
||||||
end = today.clone().weekday(6).endOf('day');
|
.clone()
|
||||||
|
.weekday(0)
|
||||||
|
.startOf('day');
|
||||||
|
end = today
|
||||||
|
.clone()
|
||||||
|
.weekday(6)
|
||||||
|
.endOf('day');
|
||||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||||
break;
|
break;
|
||||||
case DueDateFilterType.NEXT_WEEK:
|
case DueDateFilterType.NEXT_WEEK:
|
||||||
start = today.clone().weekday(0).add(7, 'day').startOf('day');
|
start = today
|
||||||
end = today.clone().weekday(6).add(7, 'day').endOf('day');
|
.clone()
|
||||||
|
.weekday(0)
|
||||||
|
.add(7, 'day')
|
||||||
|
.startOf('day');
|
||||||
|
end = today
|
||||||
|
.clone()
|
||||||
|
.weekday(6)
|
||||||
|
.add(7, 'day')
|
||||||
|
.endOf('day');
|
||||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||||
break;
|
break;
|
||||||
case DueDateFilterType.ONE_WEEK:
|
case DueDateFilterType.ONE_WEEK:
|
||||||
start = today.clone().startOf('day');
|
start = today.clone().startOf('day');
|
||||||
end = today.clone().add(7, 'day').endOf('day');
|
end = today
|
||||||
|
.clone()
|
||||||
|
.add(7, 'day')
|
||||||
|
.endOf('day');
|
||||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||||
break;
|
break;
|
||||||
case DueDateFilterType.TWO_WEEKS:
|
case DueDateFilterType.TWO_WEEKS:
|
||||||
start = today.clone().startOf('day');
|
start = today.clone().startOf('day');
|
||||||
end = today.clone().add(14, 'day').endOf('day');
|
end = today
|
||||||
|
.clone()
|
||||||
|
.add(14, 'day')
|
||||||
|
.endOf('day');
|
||||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||||
break;
|
break;
|
||||||
case DueDateFilterType.THREE_WEEKS:
|
case DueDateFilterType.THREE_WEEKS:
|
||||||
start = today.clone().startOf('day');
|
start = today.clone().startOf('day');
|
||||||
end = today.clone().add(21, 'day').endOf('day');
|
end = today
|
||||||
|
.clone()
|
||||||
|
.add(21, 'day')
|
||||||
|
.endOf('day');
|
||||||
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
isFiltered = shouldFilter(taskDueDate.isBetween(start, end));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -74,7 +104,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
|||||||
}
|
}
|
||||||
for (const member of filters.members) {
|
for (const member of filters.members) {
|
||||||
if (task.assigned) {
|
if (task.assigned) {
|
||||||
if (task.assigned.findIndex((m) => m.id === member.id) !== -1) {
|
if (task.assigned.findIndex(m => m.id === member.id) !== -1) {
|
||||||
isFiltered = ShouldFilter.VALID;
|
isFiltered = ShouldFilter.VALID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,7 +116,7 @@ export default function shouldMetaFilter(task: Task, filters: TaskMetaFilters) {
|
|||||||
}
|
}
|
||||||
for (const label of filters.labels) {
|
for (const label of filters.labels) {
|
||||||
if (task.labels) {
|
if (task.labels) {
|
||||||
if (task.labels.findIndex((m) => m.projectLabel.id === label.id) !== -1) {
|
if (task.labels.findIndex(m => m.projectLabel.id === label.id) !== -1) {
|
||||||
isFiltered = ShouldFilter.VALID;
|
isFiltered = ShouldFilter.VALID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
import AccessAccount from 'shared/undraw/AccessAccount';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
background: #eff2f7;
|
background: #eff2f7;
|
||||||
@ -16,12 +15,6 @@ export const Column = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@media (max-width: 600px) {
|
|
||||||
svg {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LoginFormWrapper = styled.div`
|
export const LoginFormWrapper = styled.div`
|
||||||
@ -32,47 +25,18 @@ export const LoginFormWrapper = styled.div`
|
|||||||
export const LoginFormContainer = styled.div`
|
export const LoginFormContainer = styled.div`
|
||||||
min-height: 505px;
|
min-height: 505px;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@media (max-width: 600px) {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
border: solid black 1px;
|
|
||||||
width: 600px;
|
|
||||||
height: 1100px;
|
|
||||||
box-shadow: 20px 20px 50px black;
|
|
||||||
}
|
|
||||||
@media (min-height: 641px) and (max-height: 653px) {
|
|
||||||
margin-top: 25%;
|
|
||||||
}
|
|
||||||
@media (min-height: 654px) and (max-height: 823px) and (max-width: 500px) {
|
|
||||||
margin-top: -20%;
|
|
||||||
}
|
|
||||||
@media (min-height: 480px) and (max-height: 639px) {
|
|
||||||
margin-top: 20%;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Title = styled.h1`
|
export const Title = styled.h1`
|
||||||
color: #ebeefd;
|
color: #ebeefd;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
@media (max-width: 600px) {
|
|
||||||
font-size: 38px;
|
|
||||||
margin-top: 50px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SubTitle = styled.h2`
|
export const SubTitle = styled.h2`
|
||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
@media (max-width: 600px) {
|
|
||||||
margin-top: 30px;
|
|
||||||
font-size: 24.5px;
|
|
||||||
margin-bottom: 90px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
export const Form = styled.form`
|
export const Form = styled.form`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -84,10 +48,6 @@ export const FormLabel = styled.label`
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
@media (max-width: 600px) {
|
|
||||||
font-size: 35px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FormTextInput = styled.input`
|
export const FormTextInput = styled.input`
|
||||||
@ -99,92 +59,28 @@ export const FormTextInput = styled.input`
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
border: 5px solid rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 5%;
|
|
||||||
font-size: 30px;
|
|
||||||
background-color: #353D64;
|
|
||||||
color: black;
|
|
||||||
padding: 0.7rem 1rem 1rem 3rem;
|
|
||||||
text-align: center;
|
|
||||||
&::placeholder {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:placeholder-shown) {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FormIcon = styled.div`
|
export const FormIcon = styled.div`
|
||||||
top: 30px;
|
top: 30px;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
svg {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: inline;
|
|
||||||
position: absolute;
|
|
||||||
top: 30px;
|
|
||||||
left: -5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FormError = styled.span`
|
export const FormError = styled.span`
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: ${(props) => props.theme.colors.danger};
|
color: ${props => props.theme.colors.danger};
|
||||||
@media (max-width: 600px) {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LoginButton = styled(Button)`
|
export const LoginButton = styled(Button)``;
|
||||||
@media (max-width: 600px) {
|
|
||||||
span {
|
|
||||||
font-size: 40px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
align-self: center;
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
margin-top: 40%;
|
|
||||||
width: 100%;
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 5px 5px 20px white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ActionButtons = styled.div`
|
export const ActionButtons = styled.div`
|
||||||
margin-top: 17.5px;
|
margin-top: 17.5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@media (max-width: 600px) {
|
|
||||||
width: 150px;
|
|
||||||
align-content: center;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RegisterButton = styled(Button)`
|
export const RegisterButton = styled(Button)``;
|
||||||
@media (max-width: 600px) {
|
|
||||||
span {
|
|
||||||
font-size: 40px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
margin-top: 29%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const LogoTitle = styled.div`
|
export const LogoTitle = styled.div`
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@ -192,9 +88,6 @@ export const LogoTitle = styled.div`
|
|||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
transition: visibility, opacity, transform 0.25s ease;
|
transition: visibility, opacity, transform 0.25s ease;
|
||||||
color: #7367f0;
|
color: #7367f0;
|
||||||
@media (max-width: 600px) {
|
|
||||||
font-size: 60px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LogoWrapper = styled.div`
|
export const LogoWrapper = styled.div`
|
||||||
@ -207,16 +100,5 @@ export const LogoWrapper = styled.div`
|
|||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
color: rgb(222, 235, 255);
|
color: rgb(222, 235, 255);
|
||||||
border-bottom: 1px solid ${(props) => mixin.rgba(props.theme.colors.alternate, 0.65)};
|
border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
|
||||||
@media (max-width: 600px) {
|
|
||||||
svg {
|
|
||||||
display: inline;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import AccessAccount from 'shared/undraw/AccessAccount';
|
import AccessAccount from 'shared/undraw/AccessAccount';
|
||||||
import { User, Lock, Taskcafe } from 'shared/icons';
|
import { User, Lock, Taskcafe } from 'shared/icons';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useHistory } from 'react-router';
|
|
||||||
import LoadingSpinner from 'shared/components/LoadingSpinner';
|
import LoadingSpinner from 'shared/components/LoadingSpinner';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -25,28 +25,11 @@ import {
|
|||||||
|
|
||||||
const Login = ({ onSubmit }: LoginProps) => {
|
const Login = ({ onSubmit }: LoginProps) => {
|
||||||
const [isComplete, setComplete] = useState(true);
|
const [isComplete, setComplete] = useState(true);
|
||||||
const [showRegistration, setShowRegistration] = useState(false);
|
const { register, handleSubmit, errors, setError, formState } = useForm<LoginFormData>();
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setError,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<LoginFormData>();
|
|
||||||
const loginSubmit = (data: LoginFormData) => {
|
const loginSubmit = (data: LoginFormData) => {
|
||||||
setComplete(false);
|
setComplete(false);
|
||||||
onSubmit(data, setComplete, setError);
|
onSubmit(data, setComplete, setError);
|
||||||
};
|
};
|
||||||
const history = useHistory();
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/settings').then(async (x) => {
|
|
||||||
const { isConfigured, allowPublicRegistration } = await x.json();
|
|
||||||
if (!isConfigured) {
|
|
||||||
history.push('/register');
|
|
||||||
} else if (allowPublicRegistration) {
|
|
||||||
setShowRegistration(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Column>
|
<Column>
|
||||||
@ -65,9 +48,10 @@ const Login = ({ onSubmit }: LoginProps) => {
|
|||||||
<FormLabel htmlFor="username">
|
<FormLabel htmlFor="username">
|
||||||
Username
|
Username
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
placeholder="Username"
|
|
||||||
type="text"
|
type="text"
|
||||||
{...register('username', { required: 'Username is required' })}
|
id="username"
|
||||||
|
name="username"
|
||||||
|
ref={register({ required: 'Username is required' })}
|
||||||
/>
|
/>
|
||||||
<FormIcon>
|
<FormIcon>
|
||||||
<User width={20} height={20} />
|
<User width={20} height={20} />
|
||||||
@ -77,9 +61,10 @@ const Login = ({ onSubmit }: LoginProps) => {
|
|||||||
<FormLabel htmlFor="password">
|
<FormLabel htmlFor="password">
|
||||||
Password
|
Password
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
placeholder="Password"
|
|
||||||
type="password"
|
type="password"
|
||||||
{...register('password', { required: 'Password is required' })}
|
id="password"
|
||||||
|
name="password"
|
||||||
|
ref={register({ required: 'Password is required' })}
|
||||||
/>
|
/>
|
||||||
<FormIcon>
|
<FormIcon>
|
||||||
<Lock width={20} height={20} />
|
<Lock width={20} height={20} />
|
||||||
@ -88,7 +73,7 @@ const Login = ({ onSubmit }: LoginProps) => {
|
|||||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||||
|
|
||||||
<ActionButtons>
|
<ActionButtons>
|
||||||
{showRegistration ? <RegisterButton variant="outline">Register</RegisterButton> : <div />}
|
<RegisterButton variant="outline">Register</RegisterButton>
|
||||||
{!isComplete && <LoadingSpinner size="32px" thickness="2px" borderSize="48px" />}
|
{!isComplete && <LoadingSpinner size="32px" thickness="2px" borderSize="48px" />}
|
||||||
<LoginButton type="submit" disabled={!isComplete}>
|
<LoginButton type="submit" disabled={!isComplete}>
|
||||||
Login
|
Login
|
||||||
|
@ -98,8 +98,8 @@ const ProjectName = styled.input`
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
||||||
box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px;
|
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const ProjectNameLabel = styled.label`
|
const ProjectNameLabel = styled.label`
|
||||||
@ -210,8 +210,8 @@ const CreateButton = styled.button`
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
border-color: ${(props) => props.theme.colors.primary};
|
border-color: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
type NewProjectProps = {
|
type NewProjectProps = {
|
||||||
@ -224,7 +224,7 @@ type NewProjectProps = {
|
|||||||
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
|
const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose, onCreateProject }) => {
|
||||||
const [projectName, setProjectName] = useState('');
|
const [projectName, setProjectName] = useState('');
|
||||||
const [team, setTeam] = useState<null | string>(initialTeamID);
|
const [team, setTeam] = useState<null | string>(initialTeamID);
|
||||||
const options = [{ label: 'No team', value: 'no-team' }, ...teams.map((t) => ({ label: t.name, value: t.id }))];
|
const options = [{ label: 'No team', value: 'no-team' }, ...teams.map(t => ({ label: t.name, value: t.id }))];
|
||||||
return (
|
return (
|
||||||
<Overlay>
|
<Overlay>
|
||||||
<Content>
|
<Content>
|
||||||
@ -234,7 +234,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeft width={16} height={16} color="#c2c6dc" />
|
<ArrowLeft color="#c2c6dc" />
|
||||||
</HeaderLeft>
|
</HeaderLeft>
|
||||||
<HeaderRight
|
<HeaderRight
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -263,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
|
|||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setTeam(e.value);
|
setTeam(e.value);
|
||||||
}}
|
}}
|
||||||
value={options.find((d) => d.value === team)}
|
value={options.find(d => d.value === team)}
|
||||||
styles={colourStyles}
|
styles={colourStyles}
|
||||||
classNamePrefix="teamSelect"
|
classNamePrefix="teamSelect"
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -1,37 +1,8 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React from 'react';
|
||||||
import styled, { css } from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import TimeAgo from 'react-timeago';
|
import TimeAgo from 'react-timeago';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { mixin } from 'shared/utils/styles';
|
|
||||||
import {
|
|
||||||
useNotificationMarkAllReadMutation,
|
|
||||||
useNotificationsQuery,
|
|
||||||
NotificationFilter,
|
|
||||||
ActionType,
|
|
||||||
useNotificationAddedSubscription,
|
|
||||||
useNotificationToggleReadMutation,
|
|
||||||
} from 'shared/generated/graphql';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
|
import { Popup } from 'shared/components/PopupMenu';
|
||||||
import { Bell, CheckCircleOutline, Circle, Ellipsis, UserCircle } from 'shared/icons';
|
|
||||||
import produce from 'immer';
|
|
||||||
import { useLocalStorage } from 'shared/hooks/useStateWithLocalStorage';
|
|
||||||
import localStorage from 'shared/utils/localStorage';
|
|
||||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
|
||||||
|
|
||||||
function getFilterMessage(filter: NotificationFilter) {
|
|
||||||
switch (filter) {
|
|
||||||
case NotificationFilter.Unread:
|
|
||||||
return 'no unread';
|
|
||||||
case NotificationFilter.Assigned:
|
|
||||||
return 'no assigned';
|
|
||||||
case NotificationFilter.Mentioned:
|
|
||||||
return 'no mentioned';
|
|
||||||
default:
|
|
||||||
return 'no';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ItemWrapper = styled.div`
|
const ItemWrapper = styled.div`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -66,7 +37,7 @@ const ItemTextContainer = styled.div`
|
|||||||
const ItemTextTitle = styled.span`
|
const ItemTextTitle = styled.span`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
display: block;
|
display: block;
|
||||||
color: ${(props) => props.theme.colors.primary};
|
color: ${props => props.theme.colors.primary};
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`;
|
`;
|
||||||
const ItemTextDesc = styled.span`
|
const ItemTextDesc = styled.span`
|
||||||
@ -101,578 +72,38 @@ export const NotificationItem: React.FC<NotificationItemProps> = ({ title, descr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NotificationHeader = styled.div`
|
const NotificationHeader = styled.div`
|
||||||
padding: 20px 28px;
|
padding: 0.75rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-top-left-radius: 6px;
|
border-top-left-radius: 6px;
|
||||||
border-top-right-radius: 6px;
|
border-top-right-radius: 6px;
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NotificationHeaderTitle = styled.span`
|
const NotificationHeaderTitle = styled.span`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EmptyMessage = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
height: 448px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const EmptyMessageLabel = styled.span`
|
|
||||||
margin-bottom: 80px;
|
|
||||||
`;
|
|
||||||
const Notifications = styled.div`
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: #414561;
|
|
||||||
height: 448px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
const NotificationFooter = styled.div`
|
const NotificationFooter = styled.div`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: ${(props) => props.theme.colors.primary};
|
color: ${props => props.theme.colors.primary};
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.bg.primary};
|
background: ${props => props.theme.colors.bg.primary};
|
||||||
}
|
}
|
||||||
border-bottom-left-radius: 6px;
|
border-bottom-left-radius: 6px;
|
||||||
border-bottom-right-radius: 6px;
|
border-bottom-right-radius: 6px;
|
||||||
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: #414561;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NotificationTabs = styled.div`
|
const NotificationPopup: React.FC = ({ children }) => {
|
||||||
align-items: flex-end;
|
|
||||||
align-self: stretch;
|
|
||||||
display: flex;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
justify-content: flex-start;
|
|
||||||
max-width: 100%;
|
|
||||||
padding-top: 4px;
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: #414561;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationTab = styled.div<{ active: boolean }>`
|
|
||||||
font-size: 80%;
|
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
|
||||||
font-size: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
line-height: normal;
|
|
||||||
min-width: 1px;
|
|
||||||
transition-duration: 0.2s;
|
|
||||||
transition-property: box-shadow, color;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 0 1 auto;
|
|
||||||
padding: 12px 16px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary};
|
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
|
||||||
}
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.active &&
|
|
||||||
css`
|
|
||||||
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
|
|
||||||
color: ${props.theme.colors.secondary};
|
|
||||||
&:hover {
|
|
||||||
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
|
|
||||||
color: ${props.theme.colors.secondary};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationLink = styled(Link)`
|
|
||||||
display: flex;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 16px 8px;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationControls = styled.div`
|
|
||||||
width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
visibility: hidden;
|
|
||||||
padding: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationButtons = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-self: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationButton = styled.div`
|
|
||||||
padding: 4px 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
&:hover svg {
|
|
||||||
fill: rgb(216, 93, 216);
|
|
||||||
stroke: rgb(216, 93, 216);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationWrapper = styled.li<{ read: boolean }>`
|
|
||||||
min-height: 80px;
|
|
||||||
display: flex;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: background-color 0.1s ease-in-out;
|
|
||||||
margin: 2px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
|
||||||
&:hover {
|
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.5)};
|
|
||||||
}
|
|
||||||
&:hover ${NotificationLink} {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
&:hover ${NotificationControls} {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
${(props) =>
|
|
||||||
!props.read &&
|
|
||||||
css`
|
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.5)};
|
|
||||||
&:hover {
|
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.6)};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationContentFooter = styled.div`
|
|
||||||
margin-top: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationCausedBy = styled.div`
|
|
||||||
height: 48px;
|
|
||||||
width: 48px;
|
|
||||||
min-height: 48px;
|
|
||||||
min-width: 48px;
|
|
||||||
`;
|
|
||||||
const NotificationCausedByInitials = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
text: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
background: #7367f0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationCausedByImage = styled.img`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
background: #7367f0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationContent = styled.div`
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-left: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationContentHeader = styled.div`
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-left: 8px;
|
|
||||||
fill: rgb(216, 93, 216);
|
|
||||||
stroke: rgb(216, 93, 216);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationBody = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #fff;
|
|
||||||
svg {
|
|
||||||
fill: rgb(216, 93, 216);
|
|
||||||
stroke: rgb(216, 93, 216);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationPrefix = styled.span`
|
|
||||||
color: rgb(216, 93, 216);
|
|
||||||
margin: 0 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationSeparator = styled.span`
|
|
||||||
margin: 0 6px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type NotificationProps = {
|
|
||||||
causedBy?: { fullname: string; username: string; id: string } | null;
|
|
||||||
createdAt: string;
|
|
||||||
read: boolean;
|
|
||||||
data: Array<{ key: string; value: string }>;
|
|
||||||
actionType: ActionType;
|
|
||||||
onToggleRead: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Notification: React.FC<NotificationProps> = ({ causedBy, createdAt, data, actionType, read, onToggleRead }) => {
|
|
||||||
const prefix: any = [];
|
|
||||||
const { hidePopup } = usePopup();
|
|
||||||
const dataMap = new Map<string, string>();
|
|
||||||
data.forEach((d) => dataMap.set(d.key, d.value));
|
|
||||||
let link = '#';
|
|
||||||
switch (actionType) {
|
|
||||||
case ActionType.TaskAssigned:
|
|
||||||
prefix.push(<UserCircle key="profile" width={14} height={16} />);
|
|
||||||
prefix.push(
|
|
||||||
<NotificationPrefix key="prefix">
|
|
||||||
<span style={{ fontWeight: 'bold' }}>{causedBy ? causedBy.fullname : 'Removed user'}</span>
|
|
||||||
</NotificationPrefix>,
|
|
||||||
);
|
|
||||||
prefix.push(<span key="content">assigned you to the task "e;{dataMap.get('TaskName')}"e;</span>);
|
|
||||||
link = `/p/${dataMap.get('ProjectID')}/board/c/${dataMap.get('TaskID')}`;
|
|
||||||
break;
|
|
||||||
case ActionType.DueDateReminder:
|
|
||||||
prefix.push(<Bell key="profile" width={14} height={16} />);
|
|
||||||
prefix.push(<NotificationPrefix key="prefix">{dataMap.get('TaskName')}</NotificationPrefix>);
|
|
||||||
const now = dayjs();
|
|
||||||
if (dayjs(dataMap.get('DueDate')).isBefore(dayjs())) {
|
|
||||||
prefix.push(
|
|
||||||
<span key="content">is due {dayjs.duration(now.diff(dayjs(dataMap.get('DueAt')))).humanize(true)}</span>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
prefix.push(
|
|
||||||
<span key="content">
|
|
||||||
has passed the due date {dayjs.duration(dayjs(dataMap.get('DueAt')).diff(now)).humanize(true)}
|
|
||||||
</span>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
link = `/p/${dataMap.get('ProjectID')}/board/c/${dataMap.get('TaskID')}`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('unknown action type');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationWrapper read={read}>
|
|
||||||
<NotificationLink to={link} onClick={hidePopup}>
|
|
||||||
<NotificationCausedBy>
|
|
||||||
<NotificationCausedByInitials>
|
|
||||||
{causedBy
|
|
||||||
? causedBy.fullname
|
|
||||||
.split(' ')
|
|
||||||
.map((n) => n[0])
|
|
||||||
.join('.')
|
|
||||||
: 'RU'}
|
|
||||||
</NotificationCausedByInitials>
|
|
||||||
</NotificationCausedBy>
|
|
||||||
<NotificationContent>
|
|
||||||
<NotificationBody>{prefix}</NotificationBody>
|
|
||||||
<NotificationContentFooter>
|
|
||||||
<span>{dayjs.duration(dayjs(createdAt).diff(dayjs())).humanize(true)}</span>
|
|
||||||
<NotificationSeparator>•</NotificationSeparator>
|
|
||||||
<span>{dataMap.get('ProjectName')}</span>
|
|
||||||
</NotificationContentFooter>
|
|
||||||
</NotificationContent>
|
|
||||||
</NotificationLink>
|
|
||||||
<NotificationControls>
|
|
||||||
<NotificationButtons>
|
|
||||||
<NotificationButton onClick={() => onToggleRead()}>
|
|
||||||
{read ? <Circle width={18} height={18} /> : <CheckCircleOutline width={18} height={18} />}
|
|
||||||
</NotificationButton>
|
|
||||||
</NotificationButtons>
|
|
||||||
</NotificationControls>
|
|
||||||
</NotificationWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PopupContent = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-color: #414561;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ label: 'All', key: NotificationFilter.All },
|
|
||||||
{ label: 'Unread', key: NotificationFilter.Unread },
|
|
||||||
{ label: 'I was mentioned', key: NotificationFilter.Mentioned },
|
|
||||||
{ label: 'Assigned to me', key: NotificationFilter.Assigned },
|
|
||||||
];
|
|
||||||
|
|
||||||
type NotificationEntry = {
|
|
||||||
id: string;
|
|
||||||
read: boolean;
|
|
||||||
readAt?: string | undefined | null;
|
|
||||||
notification: {
|
|
||||||
id: string;
|
|
||||||
data: Array<{ key: string; value: string }>;
|
|
||||||
actionType: ActionType;
|
|
||||||
causedBy?: { id: string; username: string; fullname: string } | undefined | null;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
type NotificationPopupProps = {
|
|
||||||
onToggleRead: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NotificationHeaderMenu = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
top: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationHeaderMenuIcon = styled.div`
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
svg {
|
|
||||||
fill: #fff;
|
|
||||||
stroke: #fff;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationHeaderMenuContent = styled.div<{ show: boolean }>`
|
|
||||||
min-width: 130px;
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 6px;
|
|
||||||
height: 50px;
|
|
||||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-color: #414561;
|
|
||||||
background: #262c49;
|
|
||||||
padding: 6px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NotificationHeaderMenuButton = styled.div`
|
|
||||||
position: relative;
|
|
||||||
padding-left: 4px;
|
|
||||||
padding-right: 4px;
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
&:hover {
|
|
||||||
background: ${(props) => props.theme.colors.primary};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const NotificationPopup: React.FC<NotificationPopupProps> = ({ onToggleRead }) => {
|
|
||||||
const [filter, setFilter] = useLocalStorage<NotificationFilter>(
|
|
||||||
localStorage.NOTIFICATIONS_FILTER,
|
|
||||||
NotificationFilter.Unread,
|
|
||||||
);
|
|
||||||
const [data, setData] = useState<{ nodes: Array<NotificationEntry>; hasNextPage: boolean; cursor: string }>({
|
|
||||||
nodes: [],
|
|
||||||
hasNextPage: false,
|
|
||||||
cursor: '',
|
|
||||||
});
|
|
||||||
const [toggleRead] = useNotificationToggleReadMutation({
|
|
||||||
onCompleted: (data) => {
|
|
||||||
setData((prev) => {
|
|
||||||
return produce(prev, (draft) => {
|
|
||||||
const idx = draft.nodes.findIndex((n) => n.id === data.notificationToggleRead.id);
|
|
||||||
if (idx !== -1) {
|
|
||||||
draft.nodes[idx].read = data.notificationToggleRead.read;
|
|
||||||
draft.nodes[idx].readAt = data.notificationToggleRead.readAt;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
onToggleRead();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { fetchMore } = useNotificationsQuery({
|
|
||||||
variables: { limit: 8, filter },
|
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
onCompleted: (d) => {
|
|
||||||
setData((prev) => ({
|
|
||||||
hasNextPage: d.notified.pageInfo.hasNextPage,
|
|
||||||
cursor: d.notified.pageInfo.endCursor ?? '',
|
|
||||||
nodes: [...prev.nodes, ...d.notified.notified],
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
useNotificationAddedSubscription({
|
|
||||||
onSubscriptionData: (d) => {
|
|
||||||
setData((n) => {
|
|
||||||
if (d.subscriptionData.data) {
|
|
||||||
return {
|
|
||||||
...n,
|
|
||||||
nodes: [d.subscriptionData.data.notificationAdded, ...n.nodes],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [toggleAllRead] = useNotificationMarkAllReadMutation();
|
|
||||||
|
|
||||||
const [showHeaderMenu, setShowHeaderMenu] = useState(false);
|
|
||||||
|
|
||||||
const $menuContent = useRef<HTMLDivElement>(null);
|
|
||||||
useOnOutsideClick($menuContent, true, () => setShowHeaderMenu(false), null);
|
|
||||||
return (
|
return (
|
||||||
<Popup title={null} tab={0} borders={false} padding={false}>
|
<Popup title={null} tab={0} borders={false} padding={false}>
|
||||||
<PopupContent>
|
|
||||||
<NotificationHeader>
|
<NotificationHeader>
|
||||||
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
|
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
|
||||||
<NotificationHeaderMenu>
|
|
||||||
<NotificationHeaderMenuIcon onClick={() => setShowHeaderMenu(true)}>
|
|
||||||
<Ellipsis size={18} color="#fff" vertical={false} />
|
|
||||||
<NotificationHeaderMenuContent ref={$menuContent} show={showHeaderMenu}>
|
|
||||||
<NotificationHeaderMenuButton
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowHeaderMenu(() => false);
|
|
||||||
toggleAllRead().then(() => {
|
|
||||||
setData((prev) =>
|
|
||||||
produce(prev, (draftData) => {
|
|
||||||
draftData.nodes = draftData.nodes.map((node) => ({ ...node, read: true }));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
onToggleRead();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Mark all as read
|
|
||||||
</NotificationHeaderMenuButton>
|
|
||||||
</NotificationHeaderMenuContent>
|
|
||||||
</NotificationHeaderMenuIcon>
|
|
||||||
</NotificationHeaderMenu>
|
|
||||||
</NotificationHeader>
|
</NotificationHeader>
|
||||||
<NotificationTabs>
|
<ul>{children}</ul>
|
||||||
{tabs.map((tab) => (
|
<NotificationFooter>View All</NotificationFooter>
|
||||||
<NotificationTab
|
|
||||||
key={tab.key}
|
|
||||||
onClick={() => {
|
|
||||||
if (filter !== tab.key) {
|
|
||||||
setData({ cursor: '', hasNextPage: false, nodes: [] });
|
|
||||||
setFilter(tab.key);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
active={tab.key === filter}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</NotificationTab>
|
|
||||||
))}
|
|
||||||
</NotificationTabs>
|
|
||||||
{data.nodes.length !== 0 ? (
|
|
||||||
<Notifications
|
|
||||||
onScroll={({ currentTarget }) => {
|
|
||||||
if (Math.ceil(currentTarget.scrollTop + currentTarget.clientHeight) >= currentTarget.scrollHeight) {
|
|
||||||
if (data.hasNextPage) {
|
|
||||||
console.log(`fetching more = ${data.cursor} - ${data.hasNextPage}`);
|
|
||||||
fetchMore({
|
|
||||||
variables: {
|
|
||||||
limit: 8,
|
|
||||||
filter,
|
|
||||||
cursor: data.cursor,
|
|
||||||
},
|
|
||||||
updateQuery: (prev, { fetchMoreResult }) => {
|
|
||||||
if (!fetchMoreResult) return prev;
|
|
||||||
setData((d) => ({
|
|
||||||
cursor: fetchMoreResult.notified.pageInfo.endCursor ?? '',
|
|
||||||
hasNextPage: fetchMoreResult.notified.pageInfo.hasNextPage,
|
|
||||||
nodes: [...d.nodes, ...fetchMoreResult.notified.notified],
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
notified: {
|
|
||||||
...prev.notified,
|
|
||||||
pageInfo: {
|
|
||||||
...fetchMoreResult.notified.pageInfo,
|
|
||||||
},
|
|
||||||
notified: [...prev.notified.notified, ...fetchMoreResult.notified.notified],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data.nodes.map((n) => (
|
|
||||||
<Notification
|
|
||||||
key={n.id}
|
|
||||||
read={n.read}
|
|
||||||
actionType={n.notification.actionType}
|
|
||||||
data={n.notification.data}
|
|
||||||
createdAt={n.notification.createdAt}
|
|
||||||
causedBy={n.notification.causedBy}
|
|
||||||
onToggleRead={() =>
|
|
||||||
toggleRead({
|
|
||||||
variables: { notifiedID: n.id },
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: 'Mutation',
|
|
||||||
notificationToggleRead: {
|
|
||||||
__typename: 'Notified',
|
|
||||||
id: n.id,
|
|
||||||
read: !n.read,
|
|
||||||
readAt: new Date().toUTCString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).then(() => {
|
|
||||||
onToggleRead();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Notifications>
|
|
||||||
) : (
|
|
||||||
<EmptyMessage>
|
|
||||||
<EmptyMessageLabel>You have {getFilterMessage(filter)} notifications</EmptyMessageLabel>
|
|
||||||
</EmptyMessage>
|
|
||||||
)}
|
|
||||||
</PopupContent>
|
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -218,7 +218,7 @@ export const PopupProvider: React.FC = ({ children }) => {
|
|||||||
|
|
||||||
const setTab = (newTab: number, options?: PopupOptions) => {
|
const setTab = (newTab: number, options?: PopupOptions) => {
|
||||||
setState((prevState: PopupState) =>
|
setState((prevState: PopupState) =>
|
||||||
produce(prevState, (draftState) => {
|
produce(prevState, draftState => {
|
||||||
draftState.previousTab = currentState.currentTab;
|
draftState.previousTab = currentState.currentTab;
|
||||||
draftState.currentTab = newTab;
|
draftState.currentTab = newTab;
|
||||||
if (options) {
|
if (options) {
|
||||||
@ -296,7 +296,7 @@ const PopupMenu: React.FC<Props> = ({ width, title, top, left, onClose, noHeader
|
|||||||
<Wrapper padding borders>
|
<Wrapper padding borders>
|
||||||
{onPrevious && (
|
{onPrevious && (
|
||||||
<PreviousButton onClick={onPrevious}>
|
<PreviousButton onClick={onPrevious}>
|
||||||
<AngleLeft size={16} color="#c2c6dc" />
|
<AngleLeft color="#c2c6dc" />
|
||||||
</PreviousButton>
|
</PreviousButton>
|
||||||
)}
|
)}
|
||||||
{noHeader ? (
|
{noHeader ? (
|
||||||
@ -332,7 +332,7 @@ export const Popup: React.FC<PopupProps> = ({ borders = true, padding = true, ti
|
|||||||
setTab(0);
|
setTab(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AngleLeft size={16} color="#c2c6dc" />
|
<AngleLeft color="#c2c6dc" />
|
||||||
</PreviousButton>
|
</PreviousButton>
|
||||||
)}
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
|
@ -2,15 +2,15 @@ import React, { useRef } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
|
||||||
width: ${(props) => props.size}px;
|
width: ${props => props.size}px;
|
||||||
height: ${(props) => props.size}px;
|
height: ${props => props.size}px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: ${(props) => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
`;
|
`;
|
||||||
@ -22,10 +22,6 @@ type ProfileIconProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
|
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
|
||||||
let realSize = size;
|
|
||||||
if (size === null) {
|
|
||||||
realSize = 28;
|
|
||||||
}
|
|
||||||
const $profileRef = useRef<HTMLDivElement>(null);
|
const $profileRef = useRef<HTMLDivElement>(null);
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
@ -33,7 +29,7 @@ const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size })
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onProfileClick($profileRef, user);
|
onProfileClick($profileRef, user);
|
||||||
}}
|
}}
|
||||||
size={realSize}
|
size={size}
|
||||||
backgroundURL={user.profileIcon.url ?? null}
|
backgroundURL={user.profileIcon.url ?? null}
|
||||||
bgColor={user.profileIcon.bgColor ?? null}
|
bgColor={user.profileIcon.bgColor ?? null}
|
||||||
>
|
>
|
||||||
@ -42,4 +38,8 @@ const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size })
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ProfileIcon.defaultProps = {
|
||||||
|
size: 28,
|
||||||
|
};
|
||||||
|
|
||||||
export default ProfileIcon;
|
export default ProfileIcon;
|
||||||
|
@ -38,17 +38,12 @@ export const ListSeparator = styled.hr`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
publicOn: null | string;
|
|
||||||
onDeleteProject: () => void;
|
onDeleteProject: () => void;
|
||||||
onToggleProjectVisible: (visible: boolean) => void;
|
|
||||||
};
|
};
|
||||||
const ProjectSettings: React.FC<Props> = ({ publicOn, onDeleteProject, onToggleProjectVisible }) => {
|
const ProjectSettings: React.FC<Props> = ({ onDeleteProject }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ListActionsWrapper>
|
<ListActionsWrapper>
|
||||||
<ListActionItemWrapper onClick={() => onToggleProjectVisible(publicOn === null)}>
|
|
||||||
<ListActionItem>{`Make ${publicOn === null ? 'public' : 'private'}`}</ListActionItem>
|
|
||||||
</ListActionItemWrapper>
|
|
||||||
<ListActionItemWrapper onClick={() => onDeleteProject()}>
|
<ListActionItemWrapper onClick={() => onDeleteProject()}>
|
||||||
<ListActionItem>Delete Project</ListActionItem>
|
<ListActionItem>Delete Project</ListActionItem>
|
||||||
</ListActionItemWrapper>
|
</ListActionItemWrapper>
|
||||||
@ -132,18 +127,5 @@ const DeleteConfirm: React.FC<DeleteConfirmProps> = ({ description, deletedItems
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type PublicConfirmProps = {
|
export { DeleteConfirm };
|
||||||
onConfirm: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PublicConfirm: React.FC<PublicConfirmProps> = ({ onConfirm }) => {
|
|
||||||
return (
|
|
||||||
<ConfirmWrapper>
|
|
||||||
<ConfirmDescription>Public projects can be accessed by anyone with a link to the project.</ConfirmDescription>
|
|
||||||
<ConfirmDeleteButton onClick={() => onConfirm()}>Make public</ConfirmDeleteButton>
|
|
||||||
</ConfirmWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { DeleteConfirm, PublicConfirm };
|
|
||||||
export default ProjectSettings;
|
export default ProjectSettings;
|
||||||
|
@ -26,12 +26,7 @@ const INITIALS_PATTERN = /[a-zA-Z]{2,3}/i;
|
|||||||
|
|
||||||
const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
||||||
const [isComplete, setComplete] = useState(true);
|
const [isComplete, setComplete] = useState(true);
|
||||||
const {
|
const { register, handleSubmit, errors, setError } = useForm<RegisterFormData>();
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
setError,
|
|
||||||
} = useForm<RegisterFormData>();
|
|
||||||
const loginSubmit = (data: RegisterFormData) => {
|
const loginSubmit = (data: RegisterFormData) => {
|
||||||
setComplete(false);
|
setComplete(false);
|
||||||
onSubmit(data, setComplete, setError);
|
onSubmit(data, setComplete, setError);
|
||||||
@ -60,7 +55,12 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
|||||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||||
<FormLabel htmlFor="fullname">
|
<FormLabel htmlFor="fullname">
|
||||||
Full name
|
Full name
|
||||||
<FormTextInput type="text" {...register('fullname', { required: 'Full name is required' })} />
|
<FormTextInput
|
||||||
|
type="text"
|
||||||
|
id="fullname"
|
||||||
|
name="fullname"
|
||||||
|
ref={register({ required: 'Full name is required' })}
|
||||||
|
/>
|
||||||
<FormIcon>
|
<FormIcon>
|
||||||
<User width={20} height={20} />
|
<User width={20} height={20} />
|
||||||
</FormIcon>
|
</FormIcon>
|
||||||
@ -68,7 +68,12 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
|||||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||||
<FormLabel htmlFor="username">
|
<FormLabel htmlFor="username">
|
||||||
Username
|
Username
|
||||||
<FormTextInput type="text" {...register('username', { required: 'Username is required' })} />
|
<FormTextInput
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
ref={register({ required: 'Username is required' })}
|
||||||
|
/>
|
||||||
<FormIcon>
|
<FormIcon>
|
||||||
<User width={20} height={20} />
|
<User width={20} height={20} />
|
||||||
</FormIcon>
|
</FormIcon>
|
||||||
@ -78,7 +83,9 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
|||||||
Email
|
Email
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
type="text"
|
type="text"
|
||||||
{...register('email', {
|
id="email"
|
||||||
|
name="email"
|
||||||
|
ref={register({
|
||||||
required: 'Email is required',
|
required: 'Email is required',
|
||||||
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||||
})}
|
})}
|
||||||
@ -92,7 +99,9 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
|||||||
Initials
|
Initials
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
type="text"
|
type="text"
|
||||||
{...register('initials', {
|
id="initials"
|
||||||
|
name="initials"
|
||||||
|
ref={register({
|
||||||
required: 'Initials is required',
|
required: 'Initials is required',
|
||||||
pattern: {
|
pattern: {
|
||||||
value: INITIALS_PATTERN,
|
value: INITIALS_PATTERN,
|
||||||
@ -107,7 +116,12 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
|||||||
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
{errors.initials && <FormError>{errors.initials.message}</FormError>}
|
||||||
<FormLabel htmlFor="password">
|
<FormLabel htmlFor="password">
|
||||||
Password
|
Password
|
||||||
<FormTextInput type="password" {...register('password', { required: 'Password is required' })} />
|
<FormTextInput
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
ref={register({ required: 'Password is required' })}
|
||||||
|
/>
|
||||||
<FormIcon>
|
<FormIcon>
|
||||||
<Lock width={20} height={20} />
|
<Lock width={20} height={20} />
|
||||||
</FormIcon>
|
</FormIcon>
|
||||||
@ -117,7 +131,9 @@ const Register = ({ onSubmit, registered = false }: RegisterProps) => {
|
|||||||
Password (Confirm)
|
Password (Confirm)
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
type="password"
|
type="password"
|
||||||
{...register('password_confirm', { required: 'Password (confirm) is required' })}
|
id="password_confirm"
|
||||||
|
name="password_confirm"
|
||||||
|
ref={register({ required: 'Password (confirm) is required' })}
|
||||||
/>
|
/>
|
||||||
<FormIcon>
|
<FormIcon>
|
||||||
<Lock width={20} height={20} />
|
<Lock width={20} height={20} />
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { User } from 'shared/icons';
|
import { User } from 'shared/icons';
|
||||||
|
import Input from 'shared/components/Input';
|
||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import ControlledInput from 'shared/components/ControlledInput';
|
|
||||||
|
|
||||||
const PasswordInput = styled(ControlledInput)`
|
const PasswordInput = styled(Input)`
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const UserInfoInput = styled(ControlledInput)`
|
const UserInfoInput = styled(Input)`
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FormError = styled.span`
|
const FormError = styled.span`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: ${(props) => props.theme.colors.warning};
|
color: ${props => props.theme.colors.warning};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ProfileContainer = styled.div`
|
const ProfileContainer = styled.div`
|
||||||
@ -42,7 +42,7 @@ const AvatarMask = styled.div<{ background: string }>`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: ${(props) => props.background};
|
background: ${props => props.background};
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -152,12 +152,12 @@ const TabNavItemButton = styled.button<{ active: boolean }>`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
color: ${(props) => (props.active ? `${props.theme.colors.primary}` : '#c2c6dc')};
|
color: ${props => (props.active ? `${props.theme.colors.primary}` : '#c2c6dc')};
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.theme.colors.primary};
|
color: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
&:hover svg {
|
&:hover svg {
|
||||||
fill: ${(props) => props.theme.colors.primary};
|
fill: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -173,14 +173,10 @@ const TabNavLine = styled.span<{ top: number }>`
|
|||||||
width: 2px;
|
width: 2px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
transform: scaleX(1);
|
transform: scaleX(1);
|
||||||
top: ${(props) => props.top}px;
|
top: ${props => props.top}px;
|
||||||
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(30deg, ${props => props.theme.colors.primary}, ${props => props.theme.colors.primary});
|
||||||
30deg,
|
box-shadow: 0 0 8px 0 ${props => props.theme.colors.primary};
|
||||||
${(props) => props.theme.colors.primary},
|
|
||||||
${(props) => props.theme.colors.primary}
|
|
||||||
);
|
|
||||||
box-shadow: 0 0 8px 0 ${(props) => props.theme.colors.primary};
|
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@ -271,36 +267,36 @@ type ResetPasswordTabProps = {
|
|||||||
};
|
};
|
||||||
const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword }) => {
|
const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword }) => {
|
||||||
const [active, setActive] = useState(true);
|
const [active, setActive] = useState(true);
|
||||||
const {
|
const { register, handleSubmit, errors, setError, reset } = useForm<{ password: string; password_confirm: string }>();
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
setError,
|
|
||||||
reset,
|
|
||||||
} = useForm<{ password: string; passwordConfirm: string }>();
|
|
||||||
const done = () => {
|
const done = () => {
|
||||||
reset();
|
reset();
|
||||||
setActive(true);
|
setActive(true);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit((data) => {
|
onSubmit={handleSubmit(data => {
|
||||||
if (data.password !== data.passwordConfirm) {
|
if (data.password !== data.password_confirm) {
|
||||||
setError('password', { message: 'Passwords must match!', type: 'error' });
|
setError('password', { message: 'Passwords must match!', type: 'error' });
|
||||||
setError('passwordConfirm', { message: 'Passwords must match!', type: 'error' });
|
setError('password_confirm', { message: 'Passwords must match!', type: 'error' });
|
||||||
} else {
|
} else {
|
||||||
onResetPassword(data.password, done);
|
onResetPassword(data.password, done);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<PasswordInput width="100%" {...register('password', { required: 'Password is required' })} label="Password" />
|
<PasswordInput
|
||||||
|
width="100%"
|
||||||
|
ref={register({ required: 'Password is required' })}
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
width="100%"
|
width="100%"
|
||||||
{...register('passwordConfirm', { required: 'Password is required' })}
|
ref={register({ required: 'Password is required' })}
|
||||||
label="Password (confirm)"
|
label="Password (confirm)"
|
||||||
|
name="password_confirm"
|
||||||
/>
|
/>
|
||||||
{errors.passwordConfirm && <FormError>{errors.passwordConfirm.message}</FormError>}
|
{errors.password_confirm && <FormError>{errors.password_confirm.message}</FormError>}
|
||||||
<SettingActions>
|
<SettingActions>
|
||||||
<SaveButton disabled={!active} type="submit">
|
<SaveButton disabled={!active} type="submit">
|
||||||
Save Change
|
Save Change
|
||||||
@ -311,7 +307,7 @@ const ResetPasswordTab: React.FC<ResetPasswordTabProps> = ({ onResetPassword })
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UserInfoData = {
|
type UserInfoData = {
|
||||||
fullName: string;
|
full_name: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
initials: string;
|
initials: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -333,11 +329,7 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
|
|||||||
onChangeUserInfo,
|
onChangeUserInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const [active, setActive] = useState(true);
|
const [active, setActive] = useState(true);
|
||||||
const {
|
const { register, handleSubmit, errors } = useForm<UserInfoData>();
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<UserInfoData>();
|
|
||||||
const done = () => {
|
const done = () => {
|
||||||
setActive(true);
|
setActive(true);
|
||||||
};
|
};
|
||||||
@ -349,24 +341,26 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
|
|||||||
profile={profile.profileIcon}
|
profile={profile.profileIcon}
|
||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit((data) => {
|
onSubmit={handleSubmit(data => {
|
||||||
setActive(false);
|
setActive(false);
|
||||||
onChangeUserInfo(data, done);
|
onChangeUserInfo(data, done);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<UserInfoInput
|
<UserInfoInput
|
||||||
{...register('fullName', { required: 'Full name is required' })}
|
ref={register({ required: 'Full name is required' })}
|
||||||
|
name="full_name"
|
||||||
defaultValue={profile.fullName}
|
defaultValue={profile.fullName}
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Name"
|
label="Name"
|
||||||
/>
|
/>
|
||||||
{errors.fullName && <FormError>{errors.fullName.message}</FormError>}
|
{errors.full_name && <FormError>{errors.full_name.message}</FormError>}
|
||||||
<UserInfoInput
|
<UserInfoInput
|
||||||
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
defaultValue={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
|
||||||
{...register('initials', {
|
ref={register({
|
||||||
required: 'Initials is required',
|
required: 'Initials is required',
|
||||||
pattern: { value: INITIALS_PATTERN, message: 'Intials must be between two to four characters' },
|
pattern: { value: INITIALS_PATTERN, message: 'Intials must be between two to four characters' },
|
||||||
})}
|
})}
|
||||||
|
name="initials"
|
||||||
width="100%"
|
width="100%"
|
||||||
label="Initials "
|
label="Initials "
|
||||||
/>
|
/>
|
||||||
@ -374,7 +368,8 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
|
|||||||
<UserInfoInput disabled defaultValue={profile.username ?? ''} width="100%" label="Username " />
|
<UserInfoInput disabled defaultValue={profile.username ?? ''} width="100%" label="Username " />
|
||||||
<UserInfoInput
|
<UserInfoInput
|
||||||
width="100%"
|
width="100%"
|
||||||
{...register('email', {
|
name="email"
|
||||||
|
ref={register({
|
||||||
required: 'Email is required',
|
required: 'Email is required',
|
||||||
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
pattern: { value: EMAIL_PATTERN, message: 'Must be a valid email' },
|
||||||
})}
|
})}
|
||||||
@ -382,7 +377,7 @@ const UserInfoTab: React.FC<UserInfoTabProps> = ({
|
|||||||
label="Email"
|
label="Email"
|
||||||
/>
|
/>
|
||||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||||
<UserInfoInput width="100%" {...register('bio')} defaultValue={profile.bio ?? ''} label="Bio" />
|
<UserInfoInput width="100%" name="bio" ref={register()} defaultValue={profile.bio ?? ''} label="Bio" />
|
||||||
{errors.bio && <FormError>{errors.bio.message}</FormError>}
|
{errors.bio && <FormError>{errors.bio.message}</FormError>}
|
||||||
<SettingActions>
|
<SettingActions>
|
||||||
<SaveButton disabled={!active} type="submit">
|
<SaveButton disabled={!active} type="submit">
|
||||||
|
@ -9,22 +9,14 @@ type ActivityMessageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getVariable(data: Array<TaskActivityData>, name: string) {
|
function getVariable(data: Array<TaskActivityData>, name: string) {
|
||||||
const target = data.find((d) => d.name === name);
|
const target = data.find(d => d.name === name);
|
||||||
return target ? target.value : null;
|
return target ? target.value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVariableBool(data: Array<TaskActivityData>, name: string, defaultValue = false) {
|
function renderDate(timestamp: string | null) {
|
||||||
const target = data.find((d) => d.name === name);
|
|
||||||
return target ? target.value === 'true' : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDate(timestamp: string | null, hasTime: boolean) {
|
|
||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
if (hasTime) {
|
|
||||||
return dayjs(timestamp).format('MMM D [at] h:mm A');
|
return dayjs(timestamp).format('MMM D [at] h:mm A');
|
||||||
}
|
}
|
||||||
return dayjs(timestamp).format('MMM D');
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,19 +30,13 @@ const ActivityMessage: React.FC<ActivityMessageProps> = ({ type, data }) => {
|
|||||||
message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`;
|
message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`;
|
||||||
break;
|
break;
|
||||||
case ActivityType.TaskDueDateAdded:
|
case ActivityType.TaskDueDateAdded:
|
||||||
message = `set this task to be due ${renderDate(
|
message = `set this task to be due ${renderDate(getVariable(data, 'DueDate'))}`;
|
||||||
getVariable(data, 'DueDate'),
|
|
||||||
getVariableBool(data, 'HasTime', true),
|
|
||||||
)}`;
|
|
||||||
break;
|
break;
|
||||||
case ActivityType.TaskDueDateRemoved:
|
case ActivityType.TaskDueDateRemoved:
|
||||||
message = `removed the due date from this task`;
|
message = `removed the due date from this task`;
|
||||||
break;
|
break;
|
||||||
case ActivityType.TaskDueDateChanged:
|
case ActivityType.TaskDueDateChanged:
|
||||||
message = `changed the due date of this task to ${renderDate(
|
message = `changed the due date of this task to ${renderDate(getVariable(data, 'CurDueDate'))}`;
|
||||||
getVariable(data, 'CurDueDate'),
|
|
||||||
getVariableBool(data, 'HasTime', true),
|
|
||||||
)}`;
|
|
||||||
break;
|
break;
|
||||||
case ActivityType.TaskMarkedComplete:
|
case ActivityType.TaskMarkedComplete:
|
||||||
message = `marked this task complete`;
|
message = `marked this task complete`;
|
||||||
|
@ -69,7 +69,7 @@ const CommentCreator: React.FC<CommentCreatorProps> = ({
|
|||||||
)}
|
)}
|
||||||
<CommentEditorContainer>
|
<CommentEditorContainer>
|
||||||
<CommentTextArea
|
<CommentTextArea
|
||||||
$showCommentActions={showCommentActions}
|
showCommentActions={showCommentActions}
|
||||||
placeholder="Write a comment..."
|
placeholder="Write a comment..."
|
||||||
ref={$comment}
|
ref={$comment}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
Smile,
|
Smile,
|
||||||
} from 'shared/icons';
|
} from 'shared/icons';
|
||||||
import { toArray } from 'react-emoji-render';
|
import { toArray } from 'react-emoji-render';
|
||||||
import { useCurrentUser } from 'App/context';
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
@ -82,29 +81,29 @@ import {
|
|||||||
ActivityItemComment,
|
ActivityItemComment,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
|
||||||
const TaskDetailsLoading: React.FC = () => {
|
type TaskDetailsProps = {};
|
||||||
const { user } = useCurrentUser();
|
|
||||||
|
const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<LeftSidebar>
|
<LeftSidebar>
|
||||||
<LeftSidebarContent>
|
<LeftSidebarContent>
|
||||||
<LeftSidebarSection>
|
<LeftSidebarSection>
|
||||||
<SidebarTitle>TASK GROUP</SidebarTitle>
|
<SidebarTitle>TASK GROUP</SidebarTitle>
|
||||||
<SidebarButton $loading>
|
<SidebarButton loading>
|
||||||
<SidebarSkeleton />
|
<SidebarSkeleton />
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
<DueDateTitle>DUE DATE</DueDateTitle>
|
<DueDateTitle>DUE DATE</DueDateTitle>
|
||||||
<SidebarButton $loading>
|
<SidebarButton loading>
|
||||||
<SidebarSkeleton />
|
<SidebarSkeleton />
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
</LeftSidebarSection>
|
</LeftSidebarSection>
|
||||||
<AssignedUsersSection>
|
<AssignedUsersSection>
|
||||||
<DueDateTitle>MEMBERS</DueDateTitle>
|
<DueDateTitle>MEMBERS</DueDateTitle>
|
||||||
<SidebarButton $loading>
|
<SidebarButton loading>
|
||||||
<SidebarSkeleton />
|
<SidebarSkeleton />
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
</AssignedUsersSection>
|
</AssignedUsersSection>
|
||||||
{user && (
|
|
||||||
<ExtraActionsSection>
|
<ExtraActionsSection>
|
||||||
<DueDateTitle>ACTIONS</DueDateTitle>
|
<DueDateTitle>ACTIONS</DueDateTitle>
|
||||||
<ActionButton disabled icon={<Tags width={12} height={12} />}>
|
<ActionButton disabled icon={<Tags width={12} height={12} />}>
|
||||||
@ -115,7 +114,6 @@ const TaskDetailsLoading: React.FC = () => {
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton disabled>Cover</ActionButton>
|
<ActionButton disabled>Cover</ActionButton>
|
||||||
</ExtraActionsSection>
|
</ExtraActionsSection>
|
||||||
)}
|
|
||||||
</LeftSidebarContent>
|
</LeftSidebarContent>
|
||||||
</LeftSidebar>
|
</LeftSidebar>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
@ -127,7 +125,6 @@ const TaskDetailsLoading: React.FC = () => {
|
|||||||
<span>Mark complete</span>
|
<span>Mark complete</span>
|
||||||
</MarkCompleteButton>
|
</MarkCompleteButton>
|
||||||
</HeaderLeft>
|
</HeaderLeft>
|
||||||
{user && (
|
|
||||||
<HeaderRight>
|
<HeaderRight>
|
||||||
<HeaderActionIcon>
|
<HeaderActionIcon>
|
||||||
<Paperclip width={16} height={16} />
|
<Paperclip width={16} height={16} />
|
||||||
@ -142,10 +139,9 @@ const TaskDetailsLoading: React.FC = () => {
|
|||||||
<Trash width={16} height={16} />
|
<Trash width={16} height={16} />
|
||||||
</HeaderActionIcon>
|
</HeaderActionIcon>
|
||||||
</HeaderRight>
|
</HeaderRight>
|
||||||
)}
|
|
||||||
</HeaderInnerContainer>
|
</HeaderInnerContainer>
|
||||||
<TaskDetailsTitleWrapper $loading>
|
<TaskDetailsTitleWrapper loading>
|
||||||
<TaskDetailsTitle value="" disabled $loading />
|
<TaskDetailsTitle value="" disabled loading />
|
||||||
</TaskDetailsTitleWrapper>
|
</TaskDetailsTitleWrapper>
|
||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
<InnerContentContainer>
|
<InnerContentContainer>
|
||||||
@ -155,11 +151,9 @@ const TaskDetailsLoading: React.FC = () => {
|
|||||||
</TabBarSection>
|
</TabBarSection>
|
||||||
<ActivitySection />
|
<ActivitySection />
|
||||||
</InnerContentContainer>
|
</InnerContentContainer>
|
||||||
{user && (
|
|
||||||
<CommentContainer>
|
<CommentContainer>
|
||||||
<CommentCreator disabled onCreateComment={() => null} onMemberProfile={() => null} />
|
<CommentCreator disabled onCreateComment={() => null} onMemberProfile={() => null} />
|
||||||
</CommentContainer>
|
</CommentContainer>
|
||||||
)}
|
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,6 @@ import { mixin } from 'shared/utils/styles';
|
|||||||
import Button from 'shared/components/Button';
|
import Button from 'shared/components/Button';
|
||||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
import theme from 'App/ThemeStyles';
|
import theme from 'App/ThemeStyles';
|
||||||
import { Checkmark } from 'shared/icons';
|
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -23,14 +22,14 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
|
|||||||
position: relative;
|
position: relative;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: ${(props) => props.theme.borderRadius.alternate};
|
border-radius: ${props => props.theme.borderRadius.alternate};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
& span {
|
& span {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
${(props) =>
|
${props =>
|
||||||
props.invert
|
props.invert
|
||||||
? css`
|
? css`
|
||||||
background: ${props.theme.colors.success};
|
background: ${props.theme.colors.success};
|
||||||
@ -64,7 +63,7 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
|
|||||||
color: ${props.theme.colors.success};
|
color: ${props.theme.colors.success};
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
${(props) =>
|
${props =>
|
||||||
props.invert &&
|
props.invert &&
|
||||||
css`
|
css`
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
@ -90,7 +89,7 @@ export const SidebarTitle = styled.div`
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -109,15 +108,15 @@ export const skeletonKeyframes = keyframes`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SidebarButton = styled.div<{ $loading?: boolean }>`
|
export const SidebarButton = styled.div<{ loading?: boolean }>`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
${(props) =>
|
${props =>
|
||||||
props.$loading
|
props.loading
|
||||||
? css`
|
? css`
|
||||||
background: ${props.theme.colors.bg.primary};
|
background: ${props.theme.colors.bg.primary};
|
||||||
`
|
`
|
||||||
@ -179,15 +178,15 @@ export const HeaderLeft = styled.div`
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TaskDetailsTitleWrapper = styled.div<{ $loading?: boolean }>`
|
export const TaskDetailsTitleWrapper = styled.div<{ loading?: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 8px 0 4px 0;
|
margin: 8px 0 4px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
${(props) => props.$loading && `background: ${props.theme.colors.bg.primary};`}
|
${props => props.loading && `background: ${props.theme.colors.bg.primary};`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>`
|
export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>`
|
||||||
padding: 9px 8px 7px 8px;
|
padding: 9px 8px 7px 8px;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@ -199,11 +198,8 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
&:disabled {
|
${props =>
|
||||||
opacity: 1;
|
props.loading
|
||||||
}
|
|
||||||
${(props) =>
|
|
||||||
props.$loading
|
|
||||||
? css`
|
? css`
|
||||||
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
|
background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
|
||||||
background-size: 200px 100%;
|
background-size: 200px 100%;
|
||||||
@ -211,7 +207,7 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ $loading?: boolean }>
|
|||||||
animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
|
animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
|
||||||
`
|
`
|
||||||
: css`
|
: css`
|
||||||
&:not(:disabled):hover {
|
&:hover {
|
||||||
border-color: #414561;
|
border-color: #414561;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
@ -227,7 +223,7 @@ export const DueDateTitle = styled.div`
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -238,7 +234,7 @@ export const AssignedUsersSection = styled.div`
|
|||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
border-bottom: 1px solid ${(props) => props.theme.colors.alternate};
|
border-bottom: 1px solid ${props => props.theme.colors.alternate};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
@ -256,10 +252,10 @@ export const AssignUserIcon = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
|
border: 1px solid ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
|
||||||
}
|
}
|
||||||
&:hover svg {
|
&:hover svg {
|
||||||
fill: ${(props) => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
|
fill: ${props => mixin.rgba(props.theme.colors.text.secondary, 0.75)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -274,17 +270,17 @@ export const AssignUsersButton = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid ${(props) => mixin.darken(props.theme.colors.alternate, 0.15)};
|
border: 1px solid ${props => mixin.darken(props.theme.colors.alternate, 0.15)};
|
||||||
}
|
}
|
||||||
&:hover ${AssignUserIcon} {
|
&:hover ${AssignUserIcon} {
|
||||||
border: 1px solid ${(props) => props.theme.colors.alternate};
|
border: 1px solid ${props => props.theme.colors.alternate};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const AssignUserLabel = styled.span`
|
export const AssignUserLabel = styled.span`
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ExtraActionsSection = styled.div`
|
export const ExtraActionsSection = styled.div`
|
||||||
@ -296,7 +292,7 @@ export const ExtraActionsSection = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActionButtonsTitle = styled.h3`
|
export const ActionButtonsTitle = styled.h3`
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@ -306,17 +302,16 @@ export const ActionButton = styled(Button)`
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-left: -10px;
|
margin-left: -10px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.5)};
|
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.5)};
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
& span {
|
& span {
|
||||||
position: unset;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
background: ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.75)};
|
background: ${props => mixin.rgba(props.theme.colors.bg.primary, 0.75)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -335,10 +330,10 @@ export const HeaderActionIcon = styled.div`
|
|||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
fill: ${props => mixin.rgba(props.theme.colors.text.primary, 0.75)};
|
||||||
}
|
}
|
||||||
&:hover svg {
|
&:hover svg {
|
||||||
fill: ${(props) => mixin.rgba(props.theme.colors.primary, 0.75)});
|
fill: ${props => mixin.rgba(props.theme.colors.primary, 0.75)});
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -395,7 +390,7 @@ export const MetaDetail = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const MetaDetailTitle = styled.h3`
|
export const MetaDetailTitle = styled.h3`
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@ -414,7 +409,7 @@ export const MetaDetailContent = styled.div`
|
|||||||
`;
|
`;
|
||||||
export const TaskDetailsAddLabel = styled.div`
|
export const TaskDetailsAddLabel = styled.div`
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@ -429,7 +424,7 @@ export const TaskDetailsAddLabelIcon = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: ${(props) => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@ -445,7 +440,7 @@ export const TaskDetailLabel = styled.div<{ color: string }>`
|
|||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
background-color: ${(props) => props.color};
|
background-color: ${props => props.color};
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -498,22 +493,17 @@ export const TabBarSection = styled.div`
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
padding-left: 23px;
|
padding-left: 23px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
border-bottom: 1px solid #414561;
|
border-bottom: 1px solid #414561;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TabBarItem = styled.div`
|
export const TabBarItem = styled.div`
|
||||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.primary};
|
box-shadow: inset 0 -2px ${props => props.theme.colors.primary};
|
||||||
padding: 12px 7px 14px 7px;
|
padding: 12px 7px 14px 7px;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
margin-right: 36px;
|
margin-right: 36px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
|
||||||
|
|
||||||
export const TabBarButton = styled(Button)`
|
|
||||||
padding: 6px 12px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentContainer = styled.div`
|
export const CommentContainer = styled.div`
|
||||||
@ -544,19 +534,19 @@ export const CommentProfile = styled(TaskAssignee)`
|
|||||||
align-items: normal;
|
align-items: normal;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: boolean }>`
|
export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
background: #1f243e;
|
background: #1f243e;
|
||||||
border: none;
|
border: none;
|
||||||
transition: max-height 200ms, height 200ms, min-height 200ms;
|
transition: max-height 200ms, height 200ms, min-height 200ms;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
max-height: 36px;
|
max-height: 36px;
|
||||||
${(props) =>
|
${props =>
|
||||||
props.$showCommentActions
|
props.showCommentActions
|
||||||
? css`
|
? css`
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
@ -568,7 +558,7 @@ export const CommentTextArea = styled(TextareaAutosize)<{ $showCommentActions: b
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommentEditorActions = styled.div<{ visible: boolean }>`
|
export const CommentEditorActions = styled.div<{ visible: boolean }>`
|
||||||
display: ${(props) => (props.visible ? 'flex' : 'none')};
|
display: ${props => (props.visible ? 'flex' : 'none')};
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 5px 5px 9px;
|
padding: 5px 5px 5px 9px;
|
||||||
border-top: 1px solid #414561;
|
border-top: 1px solid #414561;
|
||||||
@ -601,7 +591,7 @@ export const ActivityItemCommentAction = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
svg {
|
svg {
|
||||||
fill: ${(props) => props.theme.colors.text.primary} !important;
|
fill: ${props => props.theme.colors.text.primary} !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -621,7 +611,7 @@ export const ActivityItemHeader = styled.div<{ editable?: boolean }>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
${(props) => props.editable && 'width: 100%;'}
|
${props => props.editable && 'width: 100%;'}
|
||||||
`;
|
`;
|
||||||
export const ActivityItemHeaderUser = styled(TaskAssignee)`
|
export const ActivityItemHeaderUser = styled(TaskAssignee)`
|
||||||
align-items: start;
|
align-items: start;
|
||||||
@ -630,7 +620,7 @@ export const ActivityItemHeaderUser = styled(TaskAssignee)`
|
|||||||
export const ActivityItemHeaderTitle = styled.div`
|
export const ActivityItemHeaderTitle = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -641,8 +631,8 @@ export const ActivityItemHeaderTitleName = styled.span`
|
|||||||
|
|
||||||
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
|
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.65)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.65)};
|
||||||
margin-left: ${(props) => props.margin}px;
|
margin-left: ${props => props.margin}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActivityItemDetails = styled.div`
|
export const ActivityItemDetails = styled.div`
|
||||||
@ -656,11 +646,11 @@ export const ActivityItemComment = styled.div<{ editable: boolean }>`
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
${mixin.boxShadowCard}
|
${mixin.boxShadowCard}
|
||||||
position: relative;
|
position: relative;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
background-color: ${(props) => mixin.darken(props.theme.colors.alternate, 0.1)};
|
background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
|
||||||
${(props) => props.editable && 'width: 100%;'}
|
${props => props.editable && 'width: 100%;'}
|
||||||
|
|
||||||
& span {
|
& span {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -690,7 +680,7 @@ export const ActivityItemCommentActions = styled.div`
|
|||||||
|
|
||||||
export const ActivityItemLog = styled.span`
|
export const ActivityItemLog = styled.span`
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ViewRawButton = styled.button`
|
export const ViewRawButton = styled.button`
|
||||||
@ -701,9 +691,9 @@ export const ViewRawButton = styled.button`
|
|||||||
right: 4px;
|
right: 4px;
|
||||||
bottom: -24px;
|
bottom: -24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: ${(props) => mixin.rgba(props.theme.colors.text.primary, 0.25)};
|
color: ${props => mixin.rgba(props.theme.colors.text.primary, 0.25)};
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -719,8 +709,3 @@ export const TaskDetailsEditor = styled(TextareaAutosize)`
|
|||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const WatchedCheckmark = styled(Checkmark)`
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
`;
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { useCurrentUser } from 'App/context';
|
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
User,
|
User,
|
||||||
@ -12,7 +11,6 @@ import {
|
|||||||
CheckSquareOutline,
|
CheckSquareOutline,
|
||||||
At,
|
At,
|
||||||
Smile,
|
Smile,
|
||||||
Eye,
|
|
||||||
} from 'shared/icons';
|
} from 'shared/icons';
|
||||||
import { toArray } from 'react-emoji-render';
|
import { toArray } from 'react-emoji-render';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
@ -80,12 +78,10 @@ import {
|
|||||||
ActivityItemHeaderTitle,
|
ActivityItemHeaderTitle,
|
||||||
ActivityItemHeaderTitleName,
|
ActivityItemHeaderTitleName,
|
||||||
ActivityItemComment,
|
ActivityItemComment,
|
||||||
TabBarButton,
|
|
||||||
WatchedCheckmark,
|
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
||||||
import onDragEnd from './onDragEnd';
|
import onDragEnd from './onDragEnd';
|
||||||
import plugin from './remark';
|
import { plugin as em } from './remark';
|
||||||
import ActivityMessage from './ActivityMessage';
|
import ActivityMessage from './ActivityMessage';
|
||||||
|
|
||||||
const parseEmojis = (value: string) => {
|
const parseEmojis = (value: string) => {
|
||||||
@ -139,7 +135,7 @@ const StreamComment: React.FC<StreamCommentProps> = ({
|
|||||||
onCreateComment={onUpdateComment}
|
onCreateComment={onUpdateComment}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ReactMarkdown skipHtml plugins={[plugin]}>
|
<ReactMarkdown escapeHtml={false} plugins={[em]}>
|
||||||
{DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
|
{DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
)}
|
)}
|
||||||
@ -239,7 +235,6 @@ type TaskDetailsProps = {
|
|||||||
onToggleChecklistItem: (itemID: string, complete: boolean) => void;
|
onToggleChecklistItem: (itemID: string, complete: boolean) => void;
|
||||||
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
onToggleTaskWatch: (task: Task, watched: boolean) => void;
|
|
||||||
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||||
onCreateComment: (task: Task, message: string) => void;
|
onCreateComment: (task: Task, message: string) => void;
|
||||||
@ -261,7 +256,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
task,
|
task,
|
||||||
editableComment = null,
|
editableComment = null,
|
||||||
onDeleteChecklist,
|
onDeleteChecklist,
|
||||||
onToggleTaskWatch,
|
|
||||||
onTaskNameChange,
|
onTaskNameChange,
|
||||||
onCommentShowActions,
|
onCommentShowActions,
|
||||||
onOpenAddChecklistPopup,
|
onOpenAddChecklistPopup,
|
||||||
@ -283,7 +277,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
onToggleChecklistItem,
|
onToggleChecklistItem,
|
||||||
onMemberProfile,
|
onMemberProfile,
|
||||||
}) => {
|
}) => {
|
||||||
const { user } = useCurrentUser();
|
|
||||||
const [taskName, setTaskName] = useState(task.name);
|
const [taskName, setTaskName] = useState(task.name);
|
||||||
const [editTaskDescription, setEditTaskDescription] = useState(() => {
|
const [editTaskDescription, setEditTaskDescription] = useState(() => {
|
||||||
if (task.description) {
|
if (task.description) {
|
||||||
@ -305,7 +298,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
const activityStream: Array<{ id: string; data: { time: string; type: 'comment' | 'activity' } }> = [];
|
const activityStream: Array<{ id: string; data: { time: string; type: 'comment' | 'activity' } }> = [];
|
||||||
|
|
||||||
if (task.activity) {
|
if (task.activity) {
|
||||||
task.activity.forEach((activity) => {
|
task.activity.forEach(activity => {
|
||||||
activityStream.push({
|
activityStream.push({
|
||||||
id: activity.id,
|
id: activity.id,
|
||||||
data: {
|
data: {
|
||||||
@ -317,7 +310,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (task.comments) {
|
if (task.comments) {
|
||||||
task.comments.forEach((comment) => {
|
task.comments.forEach(comment => {
|
||||||
activityStream.push({
|
activityStream.push({
|
||||||
id: comment.id,
|
id: comment.id,
|
||||||
data: {
|
data: {
|
||||||
@ -345,14 +338,12 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<SidebarButton
|
<SidebarButton
|
||||||
ref={$dueDateBtn}
|
ref={$dueDateBtn}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (user) {
|
|
||||||
onOpenDueDatePopop(task, $dueDateBtn);
|
onOpenDueDatePopop(task, $dueDateBtn);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{task.dueDate.at ? (
|
{task.dueDate ? (
|
||||||
<SidebarButtonText>
|
<SidebarButtonText>
|
||||||
{dayjs(task.dueDate.at).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')}
|
{dayjs(task.dueDate).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')}
|
||||||
</SidebarButtonText>
|
</SidebarButtonText>
|
||||||
) : (
|
) : (
|
||||||
<SidebarButtonText>No due date</SidebarButtonText>
|
<SidebarButtonText>No due date</SidebarButtonText>
|
||||||
@ -363,24 +354,20 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<DueDateTitle>MEMBERS</DueDateTitle>
|
<DueDateTitle>MEMBERS</DueDateTitle>
|
||||||
{task.assigned && task.assigned.length !== 0 ? (
|
{task.assigned && task.assigned.length !== 0 ? (
|
||||||
<MemberList>
|
<MemberList>
|
||||||
{task.assigned.map((m) => (
|
{task.assigned.map(m => (
|
||||||
<TaskMember
|
<TaskMember
|
||||||
key={m.id}
|
key={m.id}
|
||||||
member={m}
|
member={m}
|
||||||
size={32}
|
size={32}
|
||||||
onMemberProfile={($target) => {
|
onMemberProfile={$target => {
|
||||||
if (user) {
|
|
||||||
onMemberProfile($target, m.id);
|
onMemberProfile($target, m.id);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<AssignUserIcon
|
<AssignUserIcon
|
||||||
ref={$addMemberBtn}
|
ref={$addMemberBtn}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (user) {
|
|
||||||
onOpenAddMemberPopup(task, $addMemberBtn);
|
onOpenAddMemberPopup(task, $addMemberBtn);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus width={16} height={16} />
|
<Plus width={16} height={16} />
|
||||||
@ -390,9 +377,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<AssignUsersButton
|
<AssignUsersButton
|
||||||
ref={$noMemberBtn}
|
ref={$noMemberBtn}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (user) {
|
|
||||||
onOpenAddMemberPopup(task, $noMemberBtn);
|
onOpenAddMemberPopup(task, $noMemberBtn);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AssignUserIcon>
|
<AssignUserIcon>
|
||||||
@ -402,11 +387,10 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
</AssignUsersButton>
|
</AssignUsersButton>
|
||||||
)}
|
)}
|
||||||
</AssignedUsersSection>
|
</AssignedUsersSection>
|
||||||
{user && (
|
|
||||||
<ExtraActionsSection>
|
<ExtraActionsSection>
|
||||||
<DueDateTitle>ACTIONS</DueDateTitle>
|
<DueDateTitle>ACTIONS</DueDateTitle>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
onOpenAddLabelPopup(task, $target);
|
onOpenAddLabelPopup(task, $target);
|
||||||
}}
|
}}
|
||||||
icon={<Tags width={12} height={12} />}
|
icon={<Tags width={12} height={12} />}
|
||||||
@ -414,7 +398,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
Labels
|
Labels
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
onOpenAddChecklistPopup(task, $target);
|
onOpenAddChecklistPopup(task, $target);
|
||||||
}}
|
}}
|
||||||
icon={<CheckSquareOutline width={12} height={12} />}
|
icon={<CheckSquareOutline width={12} height={12} />}
|
||||||
@ -422,16 +406,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
Checklist
|
Checklist
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton>Cover</ActionButton>
|
<ActionButton>Cover</ActionButton>
|
||||||
<ActionButton
|
|
||||||
onClick={() => {
|
|
||||||
onToggleTaskWatch(task, !task.watched);
|
|
||||||
}}
|
|
||||||
icon={<Eye width={12} height={12} />}
|
|
||||||
>
|
|
||||||
Watch {task.watched && <WatchedCheckmark width={18} height={18} />}
|
|
||||||
</ActionButton>
|
|
||||||
</ExtraActionsSection>
|
</ExtraActionsSection>
|
||||||
)}
|
|
||||||
</LeftSidebarContent>
|
</LeftSidebarContent>
|
||||||
</LeftSidebar>
|
</LeftSidebar>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
@ -439,19 +414,15 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<HeaderInnerContainer>
|
<HeaderInnerContainer>
|
||||||
<HeaderLeft>
|
<HeaderLeft>
|
||||||
<MarkCompleteButton
|
<MarkCompleteButton
|
||||||
disabled={user === null}
|
|
||||||
invert={task.complete ?? false}
|
invert={task.complete ?? false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (user) {
|
|
||||||
onToggleTaskComplete(task);
|
onToggleTaskComplete(task);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkmark width={8} height={8} />
|
<Checkmark width={8} height={8} />
|
||||||
<span>{task.complete ? 'Completed' : 'Mark complete'}</span>
|
<span>{task.complete ? 'Completed' : 'Mark complete'}</span>
|
||||||
</MarkCompleteButton>
|
</MarkCompleteButton>
|
||||||
</HeaderLeft>
|
</HeaderLeft>
|
||||||
{user && (
|
|
||||||
<HeaderRight>
|
<HeaderRight>
|
||||||
<HeaderActionIcon>
|
<HeaderActionIcon>
|
||||||
<Paperclip width={16} height={16} />
|
<Paperclip width={16} height={16} />
|
||||||
@ -466,14 +437,12 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<Trash width={16} height={16} />
|
<Trash width={16} height={16} />
|
||||||
</HeaderActionIcon>
|
</HeaderActionIcon>
|
||||||
</HeaderRight>
|
</HeaderRight>
|
||||||
)}
|
|
||||||
</HeaderInnerContainer>
|
</HeaderInnerContainer>
|
||||||
<TaskDetailsTitleWrapper>
|
<TaskDetailsTitleWrapper>
|
||||||
<TaskDetailsTitle
|
<TaskDetailsTitle
|
||||||
value={taskName}
|
value={taskName}
|
||||||
ref={$detailsTitle}
|
ref={$detailsTitle}
|
||||||
disabled={user === null}
|
onKeyDown={e => {
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if ($detailsTitle && $detailsTitle.current) {
|
if ($detailsTitle && $detailsTitle.current) {
|
||||||
@ -481,7 +450,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={(e) => {
|
onChange={e => {
|
||||||
setTaskName(e.currentTarget.value);
|
setTaskName(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
@ -494,12 +463,12 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<Labels>
|
<Labels>
|
||||||
{task.labels.length !== 0 && (
|
{task.labels.length !== 0 && (
|
||||||
<MetaDetailContent>
|
<MetaDetailContent>
|
||||||
{task.labels.map((label) => {
|
{task.labels.map(label => {
|
||||||
return (
|
return (
|
||||||
<TaskLabelItem
|
<TaskLabelItem
|
||||||
key={label.projectLabel.id}
|
key={label.projectLabel.id}
|
||||||
label={label}
|
label={label}
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
onOpenAddLabelPopup(task, $target);
|
onOpenAddLabelPopup(task, $target);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -518,7 +487,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<TaskDetailsEditor value={taskDescriptionRef.current} />
|
<TaskDetailsEditor value={taskDescriptionRef.current} />
|
||||||
) : (
|
) : (
|
||||||
<EditorContainer
|
<EditorContainer
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
if (!editTaskDescription) {
|
if (!editTaskDescription) {
|
||||||
setEditTaskDescription(true);
|
setEditTaskDescription(true);
|
||||||
}
|
}
|
||||||
@ -526,10 +495,10 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
>
|
>
|
||||||
<Editor
|
<Editor
|
||||||
defaultValue={task.description ?? ''}
|
defaultValue={task.description ?? ''}
|
||||||
readOnly={user === null || !editTaskDescription}
|
|
||||||
theme={dark}
|
theme={dark}
|
||||||
|
readOnly={!editTaskDescription}
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(value) => {
|
onChange={value => {
|
||||||
setSaveTimeout(() => {
|
setSaveTimeout(() => {
|
||||||
clearTimeout(saveTimeout);
|
clearTimeout(saveTimeout);
|
||||||
return setTimeout(saveDescription, 2000);
|
return setTimeout(saveDescription, 2000);
|
||||||
@ -544,9 +513,9 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<ViewRawButton onClick={() => setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'}</ViewRawButton>
|
<ViewRawButton onClick={() => setShowRaw(!showRaw)}>{showRaw ? 'Show editor' : 'Show raw'}</ViewRawButton>
|
||||||
</DescriptionContainer>
|
</DescriptionContainer>
|
||||||
<ChecklistSection>
|
<ChecklistSection>
|
||||||
<DragDropContext onDragEnd={(result) => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>
|
<DragDropContext onDragEnd={result => onDragEnd(result, task, onChecklistDrop, onChecklistItemDrop)}>
|
||||||
<Droppable direction="vertical" type="checklist" droppableId="root">
|
<Droppable direction="vertical" type="checklist" droppableId="root">
|
||||||
{(dropProvided) => (
|
{dropProvided => (
|
||||||
<ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}>
|
<ChecklistContainer {...dropProvided.droppableProps} ref={dropProvided.innerRef}>
|
||||||
{task.checklists &&
|
{task.checklists &&
|
||||||
task.checklists
|
task.checklists
|
||||||
@ -554,7 +523,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
.sort((a, b) => a.position - b.position)
|
.sort((a, b) => a.position - b.position)
|
||||||
.map((checklist, idx) => (
|
.map((checklist, idx) => (
|
||||||
<Draggable key={checklist.id} draggableId={checklist.id} index={idx}>
|
<Draggable key={checklist.id} draggableId={checklist.id} index={idx}>
|
||||||
{(provided) => (
|
{provided => (
|
||||||
<Checklist
|
<Checklist
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
wrapperProps={provided.draggableProps}
|
wrapperProps={provided.draggableProps}
|
||||||
@ -564,10 +533,10 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
checklistID={checklist.id}
|
checklistID={checklist.id}
|
||||||
items={checklist.items}
|
items={checklist.items}
|
||||||
onDeleteChecklist={onDeleteChecklist}
|
onDeleteChecklist={onDeleteChecklist}
|
||||||
onChangeName={(newName) => onChangeChecklistName(checklist.id, newName)}
|
onChangeName={newName => onChangeChecklistName(checklist.id, newName)}
|
||||||
onToggleItem={onToggleChecklistItem}
|
onToggleItem={onToggleChecklistItem}
|
||||||
onDeleteItem={onDeleteItem}
|
onDeleteItem={onDeleteItem}
|
||||||
onAddItem={(n) => {
|
onAddItem={n => {
|
||||||
if (task.checklists) {
|
if (task.checklists) {
|
||||||
let position = 65535;
|
let position = 65535;
|
||||||
const [lastItem] = checklist.items
|
const [lastItem] = checklist.items
|
||||||
@ -582,7 +551,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
onChangeItemName={onChangeItemName}
|
onChangeItemName={onChangeItemName}
|
||||||
>
|
>
|
||||||
<Droppable direction="vertical" type="checklistItem" droppableId={checklist.id}>
|
<Droppable direction="vertical" type="checklistItem" droppableId={checklist.id}>
|
||||||
{(checklistDrop) => (
|
{checklistDrop => (
|
||||||
<>
|
<>
|
||||||
<ChecklistItems ref={checklistDrop.innerRef} {...checklistDrop.droppableProps}>
|
<ChecklistItems ref={checklistDrop.innerRef} {...checklistDrop.droppableProps}>
|
||||||
{checklist.items
|
{checklist.items
|
||||||
@ -590,7 +559,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
.sort((a, b) => a.position - b.position)
|
.sort((a, b) => a.position - b.position)
|
||||||
.map((item, itemIdx) => (
|
.map((item, itemIdx) => (
|
||||||
<Draggable key={item.id} draggableId={item.id} index={itemIdx}>
|
<Draggable key={item.id} draggableId={item.id} index={itemIdx}>
|
||||||
{(itemDrop) => (
|
{itemDrop => (
|
||||||
<ChecklistItem
|
<ChecklistItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
itemID={item.id}
|
itemID={item.id}
|
||||||
@ -628,34 +597,30 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
|||||||
<TabBarItem>Activity</TabBarItem>
|
<TabBarItem>Activity</TabBarItem>
|
||||||
</TabBarSection>
|
</TabBarSection>
|
||||||
<ActivitySection>
|
<ActivitySection>
|
||||||
{activityStream.map((stream) =>
|
{activityStream.map(stream =>
|
||||||
stream.data.type === 'comment' ? (
|
stream.data.type === 'comment' ? (
|
||||||
<StreamComment
|
<StreamComment
|
||||||
key={stream.id}
|
|
||||||
onExtraActions={onCommentShowActions}
|
onExtraActions={onCommentShowActions}
|
||||||
onCancelCommentEdit={onCancelCommentEdit}
|
onCancelCommentEdit={onCancelCommentEdit}
|
||||||
onUpdateComment={(message) => onUpdateComment(stream.id, message)}
|
onUpdateComment={message => onUpdateComment(stream.id, message)}
|
||||||
editable={stream.id === editableComment}
|
editable={stream.id === editableComment}
|
||||||
comment={task.comments && task.comments.find((comment) => comment.id === stream.id)}
|
comment={task.comments && task.comments.find(comment => comment.id === stream.id)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StreamActivity
|
<StreamActivity activity={task.activity && task.activity.find(activity => activity.id === stream.id)} />
|
||||||
key={stream.id}
|
|
||||||
activity={task.activity && task.activity.find((activity) => activity.id === stream.id)}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</ActivitySection>
|
</ActivitySection>
|
||||||
</InnerContentContainer>
|
</InnerContentContainer>
|
||||||
{me && (
|
|
||||||
<CommentContainer>
|
<CommentContainer>
|
||||||
|
{me && (
|
||||||
<CommentCreator
|
<CommentCreator
|
||||||
me={me}
|
me={me}
|
||||||
onCreateComment={(message) => onCreateComment(task, message)}
|
onCreateComment={message => onCreateComment(task, message)}
|
||||||
onMemberProfile={onMemberProfile}
|
onMemberProfile={onMemberProfile}
|
||||||
/>
|
/>
|
||||||
</CommentContainer>
|
|
||||||
)}
|
)}
|
||||||
|
</CommentContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { visit } from 'unist-util-visit';
|
import visit from 'unist-util-visit';
|
||||||
import emoji from 'node-emoji';
|
import emoji from 'node-emoji';
|
||||||
import { emoticon } from 'emoticon';
|
import emoticon from 'emoticon';
|
||||||
import { Emoji } from 'emoji-mart';
|
import { Emoji } from 'emoji-mart';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@ -15,31 +15,34 @@ const DEFAULT_SETTINGS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function plugin(options) {
|
function plugin(options) {
|
||||||
const settings = { ...DEFAULT_SETTINGS, ...options };
|
const settings = Object.assign({}, DEFAULT_SETTINGS, options);
|
||||||
const pad = !!settings.padSpaceAfter;
|
const pad = !!settings.padSpaceAfter;
|
||||||
const emoticonEnable = !!settings.emoticon;
|
const emoticonEnable = !!settings.emoticon;
|
||||||
|
|
||||||
function getEmojiByShortCode(match) {
|
function getEmojiByShortCode(match) {
|
||||||
// find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-),
|
// find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-),
|
||||||
const iconFull = emoticon.find((e) => e.emoticons.includes(match)); // full match
|
const iconFull = emoticon.find(e => e.emoticons.includes(match)); // full match
|
||||||
const iconPart = emoticon.find((e) => e.emoticons.includes(match.slice(0, -1))); // second search pattern
|
const iconPart = emoticon.find(e => e.emoticons.includes(match.slice(0, -1))); // second search pattern
|
||||||
const trimmedChar = iconPart ? match.slice(-1) : '';
|
const trimmedChar = iconPart ? match.slice(-1) : '';
|
||||||
const addPad = pad ? ' ' : '';
|
const addPad = pad ? ' ' : '';
|
||||||
const icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar;
|
let icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar;
|
||||||
return icon || match;
|
return icon || match;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEmoji(match) {
|
function getEmoji(match) {
|
||||||
|
console.log(match);
|
||||||
const got = emoji.get(match);
|
const got = emoji.get(match);
|
||||||
if (pad && got !== match) {
|
if (pad && got !== match) {
|
||||||
return `${got} `;
|
return got + ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(got);
|
||||||
return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />);
|
return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformer(tree) {
|
function transformer(tree) {
|
||||||
visit(tree, 'paragraph', function(node) {
|
visit(tree, 'paragraph', function(node) {
|
||||||
|
console.log(tree);
|
||||||
// node.value = node.value.replace(RE_EMOJI, getEmoji);
|
// node.value = node.value.replace(RE_EMOJI, getEmoji);
|
||||||
// jnode.type = 'html';
|
// jnode.type = 'html';
|
||||||
// jnode.tagName = 'div';
|
// jnode.tagName = 'div';
|
||||||
@ -55,10 +58,11 @@ function plugin(options) {
|
|||||||
if (emoticonEnable) {
|
if (emoticonEnable) {
|
||||||
// node.value = node.value.replace(RE_SHORT, getEmojiByShortCode);
|
// node.value = node.value.replace(RE_SHORT, getEmojiByShortCode);
|
||||||
}
|
}
|
||||||
|
console.log(node);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformer;
|
return transformer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default plugin;
|
export { plugin };
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import React, { useRef, useState, useEffect } from 'react';
|
|
||||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle, ListUnordered } from 'shared/icons';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { RoleCode } from 'shared/generated/graphql';
|
|
||||||
import * as S from './Styles';
|
|
||||||
|
|
||||||
export type MenuItem = {
|
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NavBarProps = {
|
|
||||||
menuType?: Array<MenuItem> | null;
|
|
||||||
name: string | null;
|
|
||||||
match: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NavBar: React.FC<NavBarProps> = ({ menuType, name, match }) => {
|
|
||||||
return (
|
|
||||||
<S.NavbarWrapper>
|
|
||||||
<S.NavbarHeader>
|
|
||||||
<S.ProjectActions>
|
|
||||||
<S.ProjectSwitch>
|
|
||||||
<S.ProjectSwitchInner>
|
|
||||||
<S.TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
|
|
||||||
</S.ProjectSwitchInner>
|
|
||||||
</S.ProjectSwitch>
|
|
||||||
<S.ProjectInfo>
|
|
||||||
<S.ProjectMeta>{name && <S.ProjectName>{name}</S.ProjectName>}</S.ProjectMeta>
|
|
||||||
{name && (
|
|
||||||
<S.ProjectTabs>
|
|
||||||
{menuType &&
|
|
||||||
menuType.map((menu, idx) => {
|
|
||||||
return (
|
|
||||||
<S.ProjectTab
|
|
||||||
key={menu.name}
|
|
||||||
to={menu.link}
|
|
||||||
exact
|
|
||||||
onClick={() => {
|
|
||||||
// TODO
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{menu.name}
|
|
||||||
</S.ProjectTab>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</S.ProjectTabs>
|
|
||||||
)}
|
|
||||||
</S.ProjectInfo>
|
|
||||||
</S.ProjectActions>
|
|
||||||
<S.LogoContainer to="/">
|
|
||||||
<S.TaskcafeTitle>Taskcafé</S.TaskcafeTitle>
|
|
||||||
</S.LogoContainer>
|
|
||||||
<S.GlobalActions>
|
|
||||||
<Link
|
|
||||||
to={{
|
|
||||||
pathname: '/login',
|
|
||||||
state: { redirect: match },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<S.SignIn>Sign In</S.SignIn>
|
|
||||||
</Link>
|
|
||||||
</S.GlobalActions>
|
|
||||||
</S.NavbarHeader>
|
|
||||||
</S.NavbarWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NavBar;
|
|
@ -6,11 +6,11 @@ import { NavLink, Link } from 'react-router-dom';
|
|||||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||||
|
|
||||||
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
|
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
|
||||||
z-index: ${(props) => props.zIndex};
|
z-index: ${props => props.zIndex};
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.bg.primary},
|
box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.primary},
|
||||||
inset 0 0 0 1px ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
|
inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const NavbarWrapper = styled.div`
|
export const NavbarWrapper = styled.div`
|
||||||
@ -27,9 +27,9 @@ export const NavbarHeader = styled.header`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: ${(props) => props.theme.colors.bg.primary};
|
background: ${props => props.theme.colors.bg.primary};
|
||||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
|
||||||
border-bottom: 1px solid ${(props) => mixin.rgba(props.theme.colors.alternate, 0.65)};
|
border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
|
||||||
`;
|
`;
|
||||||
export const Breadcrumbs = styled.div`
|
export const Breadcrumbs = styled.div`
|
||||||
color: rgb(94, 108, 132);
|
color: rgb(94, 108, 132);
|
||||||
@ -59,7 +59,7 @@ export const ProjectSwitchInner = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
background-color: ${(props) => props.theme.colors.primary};
|
background-color: ${props => props.theme.colors.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ProjectSwitch = styled.div`
|
export const ProjectSwitch = styled.div`
|
||||||
@ -109,27 +109,10 @@ export const NavbarLink = styled(Link)`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const NotificationCount = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: -6px;
|
|
||||||
right: -6px;
|
|
||||||
background: #7367f0;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 3px solid rgb(16, 22, 58);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IconContainerWrapper = styled.div<{ disabled?: boolean }>`
|
export const IconContainerWrapper = styled.div<{ disabled?: boolean }>`
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
${(props) =>
|
${props =>
|
||||||
props.disabled &&
|
props.disabled &&
|
||||||
css`
|
css`
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@ -159,15 +142,15 @@ export const ProfileIcon = styled.div<{
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: ${(props) => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ProjectMeta = styled.div<{ nameOnly?: boolean }>`
|
export const ProjectMeta = styled.div<{ nameOnly?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
${(props) => !props.nameOnly && 'padding-top: 9px;'}
|
${props => !props.nameOnly && 'padding-top: 9px;'}
|
||||||
margin-left: -6px;
|
margin-left: -14px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-height: 51px;
|
min-height: 51px;
|
||||||
@ -184,7 +167,7 @@ export const ProjectTabs = styled.div`
|
|||||||
|
|
||||||
export const ProjectTab = styled(NavLink)`
|
export const ProjectTab = styled(NavLink)`
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -201,22 +184,22 @@ export const ProjectTab = styled(NavLink)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary};
|
box-shadow: inset 0 -2px ${props => props.theme.colors.text.secondary};
|
||||||
color: ${(props) => props.theme.colors.text.secondary};
|
color: ${props => props.theme.colors.text.secondary};
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary};
|
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
|
||||||
color: ${(props) => props.theme.colors.secondary};
|
color: ${props => props.theme.colors.secondary};
|
||||||
}
|
}
|
||||||
&.active:hover {
|
&.active:hover {
|
||||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary};
|
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
|
||||||
color: ${(props) => props.theme.colors.secondary};
|
color: ${props => props.theme.colors.secondary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ProjectName = styled.h1`
|
export const ProjectName = styled.h1`
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding: 3px 10px 3px 8px;
|
padding: 3px 10px 3px 8px;
|
||||||
@ -258,7 +241,7 @@ export const ProjectNameTextarea = styled.input`
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding: 3px 10px 3px 8px;
|
padding: 3px 10px 3px 8px;
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px;
|
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -276,7 +259,7 @@ export const ProjectSwitcher = styled.button`
|
|||||||
color: #c2c6dc;
|
color: #c2c6dc;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -300,7 +283,7 @@ export const ProjectSettingsButton = styled.button`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${(props) => props.theme.colors.primary};
|
background: ${props => props.theme.colors.primary};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -314,19 +297,9 @@ export const ProjectFinder = styled(Button)`
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SignUp = styled(Button)`
|
|
||||||
margin-right: 8px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SignIn = styled(Button)`
|
|
||||||
margin-right: 20px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const NavSeparator = styled.div`
|
export const NavSeparator = styled.div`
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: ${(props) => props.theme.colors.border};
|
background: ${props => props.theme.colors.border};
|
||||||
height: 34px;
|
height: 34px;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
`;
|
`;
|
||||||
@ -343,11 +316,11 @@ export const LogoContainer = styled(Link)`
|
|||||||
|
|
||||||
export const TaskcafeTitle = styled.h2`
|
export const TaskcafeTitle = styled.h2`
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
color: ${(props) => props.theme.colors.text.primary};
|
color: ${props => props.theme.colors.text.primary};
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TaskcafeLogo = styled(Taskcafe)`
|
export const TaskcafeLogo = styled(Taskcafe)`
|
||||||
fill: ${(props) => props.theme.colors.text.primary};
|
fill: ${props => props.theme.colors.text.primary};
|
||||||
stroke: ${(props) => props.theme.colors.text.primary};
|
stroke: ${props => props.theme.colors.text.primary};
|
||||||
`;
|
`;
|
||||||
|
@ -36,7 +36,6 @@ import {
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
ProjectMembers,
|
ProjectMembers,
|
||||||
ProjectSwitchInner,
|
ProjectSwitchInner,
|
||||||
NotificationCount,
|
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
|
||||||
type IconContainerProps = {
|
type IconContainerProps = {
|
||||||
@ -145,7 +144,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
|||||||
</ProjectSettingsButton>
|
</ProjectSettingsButton>
|
||||||
{onFavorite && (
|
{onFavorite && (
|
||||||
<ProjectSettingsButton onClick={() => onFavorite()}>
|
<ProjectSettingsButton onClick={() => onFavorite()}>
|
||||||
<Star filled width={16} height={16} color="#c2c6dc" />
|
<Star width={16} height={16} color="#c2c6dc" />
|
||||||
</ProjectSettingsButton>
|
</ProjectSettingsButton>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -186,7 +185,6 @@ type NavBarProps = {
|
|||||||
projectMembers?: Array<TaskUser> | null;
|
projectMembers?: Array<TaskUser> | null;
|
||||||
projectInvitedMembers?: Array<InvitedUser> | null;
|
projectInvitedMembers?: Array<InvitedUser> | null;
|
||||||
|
|
||||||
hasUnread: boolean;
|
|
||||||
onRemoveFromBoard?: (userID: string) => void;
|
onRemoveFromBoard?: (userID: string) => void;
|
||||||
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||||
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
|
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
|
||||||
@ -205,7 +203,6 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
onOpenProjectFinder,
|
onOpenProjectFinder,
|
||||||
onFavorite,
|
onFavorite,
|
||||||
onSetTab,
|
onSetTab,
|
||||||
hasUnread,
|
|
||||||
projectInvitedMembers,
|
projectInvitedMembers,
|
||||||
onChangeRole,
|
onChangeRole,
|
||||||
name,
|
name,
|
||||||
@ -231,7 +228,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
<NavbarWrapper>
|
<NavbarWrapper>
|
||||||
<NavbarHeader>
|
<NavbarHeader>
|
||||||
<ProjectActions>
|
<ProjectActions>
|
||||||
<ProjectSwitch ref={$finder} onClick={(e) => onOpenProjectFinder($finder)}>
|
<ProjectSwitch ref={$finder} onClick={e => onOpenProjectFinder($finder)}>
|
||||||
<ProjectSwitchInner>
|
<ProjectSwitchInner>
|
||||||
<TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
|
<TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
|
||||||
</ProjectSwitchInner>
|
</ProjectSwitchInner>
|
||||||
@ -307,7 +304,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
))}
|
))}
|
||||||
{canInviteUser && (
|
{canInviteUser && (
|
||||||
<InviteButton
|
<InviteButton
|
||||||
onClick={($target) => {
|
onClick={$target => {
|
||||||
if (onInviteUser) {
|
if (onInviteUser) {
|
||||||
onInviteUser($target);
|
onInviteUser($target);
|
||||||
}
|
}
|
||||||
@ -333,9 +330,8 @@ const NavBar: React.FC<NavBarProps> = ({
|
|||||||
<IconContainer disabled onClick={NOOP}>
|
<IconContainer disabled onClick={NOOP}>
|
||||||
<ListUnordered width={20} height={20} />
|
<ListUnordered width={20} height={20} />
|
||||||
</IconContainer>
|
</IconContainer>
|
||||||
<IconContainer onClick={onNotificationClick}>
|
<IconContainer disabled onClick={onNotificationClick}>
|
||||||
<Bell width={20} height={20} />
|
<Bell color="#c2c6dc" size={20} />
|
||||||
{hasUnread && <NotificationCount />}
|
|
||||||
</IconContainer>
|
</IconContainer>
|
||||||
<IconContainer disabled onClick={NOOP}>
|
<IconContainer disabled onClick={NOOP}>
|
||||||
<BarChart width={20} height={20} />
|
<BarChart width={20} height={20} />
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
|||||||
mutation createProject($teamID: UUID, $name: String!) {
|
mutation createProject($teamID: UUID, $name: String!) {
|
||||||
createProject(input: {teamID: $teamID, name: $name}) {
|
createProject(input: {teamID: $teamID, name: $name}) {
|
||||||
id
|
id
|
||||||
shortId
|
|
||||||
name
|
name
|
||||||
team {
|
team {
|
||||||
id
|
id
|
||||||
|
@ -2,11 +2,9 @@ import gql from 'graphql-tag';
|
|||||||
import TASK_FRAGMENT from './fragments/task';
|
import TASK_FRAGMENT from './fragments/task';
|
||||||
|
|
||||||
const FIND_PROJECT_QUERY = gql`
|
const FIND_PROJECT_QUERY = gql`
|
||||||
query findProject($projectID: String!) {
|
query findProject($projectID: UUID!) {
|
||||||
findProject(input: { projectShortID: $projectID }) {
|
findProject(input: { projectID: $projectID }) {
|
||||||
id
|
|
||||||
name
|
name
|
||||||
publicOn
|
|
||||||
team {
|
team {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
query findTask($taskID: String!) {
|
query findTask($taskID: UUID!) {
|
||||||
findTask(input: {taskShortID: $taskID}) {
|
findTask(input: {taskID: $taskID}) {
|
||||||
id
|
id
|
||||||
shortId
|
|
||||||
name
|
name
|
||||||
watched
|
|
||||||
description
|
description
|
||||||
dueDate {
|
dueDate
|
||||||
at
|
|
||||||
notifications {
|
|
||||||
id
|
|
||||||
period
|
|
||||||
duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
position
|
position
|
||||||
complete
|
complete
|
||||||
hasTime
|
hasTime
|
||||||
|
@ -3,15 +3,11 @@ import gql from 'graphql-tag';
|
|||||||
const TASK_FRAGMENT = gql`
|
const TASK_FRAGMENT = gql`
|
||||||
fragment TaskFields on Task {
|
fragment TaskFields on Task {
|
||||||
id
|
id
|
||||||
shortId
|
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
dueDate {
|
dueDate
|
||||||
at
|
|
||||||
}
|
|
||||||
hasTime
|
hasTime
|
||||||
complete
|
complete
|
||||||
watched
|
|
||||||
completedAt
|
completedAt
|
||||||
position
|
position
|
||||||
badges {
|
badges {
|
||||||
@ -19,10 +15,6 @@ const TASK_FRAGMENT = gql`
|
|||||||
complete
|
complete
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
comments {
|
|
||||||
unread
|
|
||||||
total
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
taskGroup {
|
taskGroup {
|
||||||
id
|
id
|
||||||
|
@ -10,7 +10,6 @@ query getProjects {
|
|||||||
}
|
}
|
||||||
projects {
|
projects {
|
||||||
id
|
id
|
||||||
shortId
|
|
||||||
name
|
name
|
||||||
team {
|
team {
|
||||||
id
|
id
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
import TASK_FRAGMENT from './fragments/task';
|
|
||||||
|
|
||||||
const FIND_PROJECT_QUERY = gql`
|
|
||||||
query labels($projectID: UUID!) {
|
|
||||||
findProject(input: { projectID: $projectID }) {
|
|
||||||
labels {
|
|
||||||
id
|
|
||||||
createdDate
|
|
||||||
name
|
|
||||||
labelColor {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
colorHex
|
|
||||||
position
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
labelColors {
|
|
||||||
id
|
|
||||||
position
|
|
||||||
colorHex
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
@ -6,15 +6,12 @@ query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
|
|||||||
myTasks(input: { status: $status, sort: $sort }) {
|
myTasks(input: { status: $status, sort: $sort }) {
|
||||||
tasks {
|
tasks {
|
||||||
id
|
id
|
||||||
shortId
|
|
||||||
taskGroup {
|
taskGroup {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
name
|
name
|
||||||
dueDate {
|
dueDate
|
||||||
at
|
|
||||||
}
|
|
||||||
hasTime
|
hasTime
|
||||||
complete
|
complete
|
||||||
completedAt
|
completedAt
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
const CREATE_TASK_MUTATION = gql`
|
|
||||||
mutation notificationToggleRead($notifiedID: UUID!) {
|
|
||||||
notificationToggleRead(input: { notifiedID: $notifiedID }) {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
readAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default CREATE_TASK_MUTATION;
|
|
@ -1,34 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
export const TOP_NAVBAR_QUERY = gql`
|
|
||||||
query notifications($limit: Int!, $cursor: String, $filter: NotificationFilter!) {
|
|
||||||
notified(input: { limit: $limit, cursor: $cursor, filter: $filter }) {
|
|
||||||
totalCount
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
notified {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
readAt
|
|
||||||
notification {
|
|
||||||
id
|
|
||||||
actionType
|
|
||||||
data {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
causedBy {
|
|
||||||
username
|
|
||||||
fullname
|
|
||||||
id
|
|
||||||
}
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default TOP_NAVBAR_QUERY;
|
|
@ -1,11 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
const CREATE_TASK_MUTATION = gql`
|
|
||||||
mutation notificationMarkAllRead {
|
|
||||||
notificationMarkAllRead {
|
|
||||||
success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default CREATE_TASK_MUTATION;
|
|
@ -1,26 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
import TASK_FRAGMENT from './fragments/task';
|
|
||||||
|
|
||||||
const FIND_PROJECT_QUERY = gql`
|
|
||||||
subscription notificationAdded {
|
|
||||||
notificationAdded {
|
|
||||||
id
|
|
||||||
read
|
|
||||||
readAt
|
|
||||||
notification {
|
|
||||||
id
|
|
||||||
actionType
|
|
||||||
data {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
causedBy {
|
|
||||||
username
|
|
||||||
fullname
|
|
||||||
id
|
|
||||||
}
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
@ -1,12 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
const CREATE_TASK_MUTATION = gql`
|
|
||||||
mutation toggleTaskWatch($taskID: UUID!) {
|
|
||||||
toggleTaskWatch(input: { taskID: $taskID }) {
|
|
||||||
id
|
|
||||||
watched
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default CREATE_TASK_MUTATION;
|
|
@ -1,14 +0,0 @@
|
|||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
export const DELETE_PROJECT_MUTATION = gql`
|
|
||||||
mutation toggleProjectVisibility($projectID: UUID!, $isPublic: Boolean!) {
|
|
||||||
toggleProjectVisibility(input: { projectID: $projectID, isPublic: $isPublic }) {
|
|
||||||
project {
|
|
||||||
id
|
|
||||||
publicOn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default DELETE_PROJECT_MUTATION;
|
|
@ -3,19 +3,20 @@ import gql from 'graphql-tag';
|
|||||||
export const TOP_NAVBAR_QUERY = gql`
|
export const TOP_NAVBAR_QUERY = gql`
|
||||||
query topNavbar {
|
query topNavbar {
|
||||||
notifications {
|
notifications {
|
||||||
id
|
|
||||||
read
|
|
||||||
readAt
|
|
||||||
notification {
|
|
||||||
id
|
|
||||||
actionType
|
|
||||||
causedBy {
|
|
||||||
username
|
|
||||||
fullname
|
|
||||||
id
|
|
||||||
}
|
|
||||||
createdAt
|
createdAt
|
||||||
|
read
|
||||||
|
id
|
||||||
|
entity {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
name
|
||||||
}
|
}
|
||||||
|
actor {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
name
|
||||||
|
}
|
||||||
|
actionType
|
||||||
}
|
}
|
||||||
me {
|
me {
|
||||||
user {
|
user {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user