Compare commits
	
		
			1 Commits
		
	
	
		
			0.3.2
			...
			refactor/c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2de48e288b | 
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,8 +0,0 @@
 | 
			
		||||
blank_issues_enabled: false
 | 
			
		||||
contact_links:
 | 
			
		||||
  - name: Feature Request
 | 
			
		||||
    url: https://github.com/JordanKnott/taskcafe/discussions/new?category=ideas
 | 
			
		||||
    about: Share ideas for new features
 | 
			
		||||
  - name: Ask a Question
 | 
			
		||||
    url: https://github.com/JordanKnott/taskcafe/discussions/new?category=q-a
 | 
			
		||||
    about: Ask the community for help
 | 
			
		||||
							
								
								
									
										30
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
---
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Create a feature request to help improve Taskcafe
 | 
			
		||||
title: ""
 | 
			
		||||
labels: ""
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Is your feature request related to a problem? Please describe.**
 | 
			
		||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
A clear and concise description of what you want to happen.
 | 
			
		||||
 | 
			
		||||
**Describe alternatives you've considered**
 | 
			
		||||
A clear and concise description of any alternative solutions or features you've considered.
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context or screenshots about the feature request here.
 | 
			
		||||
 | 
			
		||||
<!--
 | 
			
		||||
 | 
			
		||||
Be aware, not all feature requests will get accepted.
 | 
			
		||||
 | 
			
		||||
Please read the contributing guide before working on any new pull requests!
 | 
			
		||||
 | 
			
		||||
If you would like to ask a question regarding a possible bug or feature request, please
 | 
			
		||||
join the Taskcafe discord - https://discord.gg/JkQDruh
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
@@ -21,4 +21,4 @@ windows:
 | 
			
		||||
  - database:
 | 
			
		||||
      root: ./
 | 
			
		||||
      panes:
 | 
			
		||||
        - pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe
 | 
			
		||||
        - pgcli postgres://taskcafe:taskcafe_test@localhost:5432/taskcafe
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,7 @@ Thanks for wanting to contribute to Taskcafe!
 | 
			
		||||
 | 
			
		||||
So you want to contribute to Taskcafe? Great!
 | 
			
		||||
 | 
			
		||||
If you have noticed a bug, please [create an issue](https://github.com/JordanKnott/taskcafe/issues/new/choose) for it before starting any work on a pull request.
 | 
			
		||||
 | 
			
		||||
If there is a [new feature you'd like added](https://github.com/JordanKnott/taskcafe/discussions/new?category=ideas) or [have a question](https://github.com/JordanKnott/taskcafe/discussions/new?category=q-a), please visit the [discussions section](https://github.com/JordanKnott/taskcafe/discussions)
 | 
			
		||||
If you have noticed a bug or want to add a new feature, please [create an issue](https://github.com/JordanKnott/taskcafe/issues/new/choose) for it before starting any work on a pull request.
 | 
			
		||||
 | 
			
		||||
Alternatively you can join the [Taskcafe discord](https://discord.gg/JkQDruh) and ask in the #questions channel.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Pipfile
									
									
									
									
									
								
							@@ -9,4 +9,4 @@ verify_ssl = true
 | 
			
		||||
pre-commit = "*"
 | 
			
		||||
 | 
			
		||||
[requires]
 | 
			
		||||
python_version = "3.9"
 | 
			
		||||
python_version = "3.8"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										71
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@@ -1,11 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
    "_meta": {
 | 
			
		||||
        "hash": {
 | 
			
		||||
            "sha256": "76a59164ad995ef4d02794470696e6f1dd199ede126c2d92a2bc1011eb288f69"
 | 
			
		||||
            "sha256": "83ec7c0175ee9763b335b1855d3d226b2fe799fcd4cafd8e08eb7294cb5ddd07"
 | 
			
		||||
        },
 | 
			
		||||
        "pipfile-spec": 6,
 | 
			
		||||
        "requires": {
 | 
			
		||||
            "python_version": "3.9"
 | 
			
		||||
            "python_version": "3.8"
 | 
			
		||||
        },
 | 
			
		||||
        "sources": [
 | 
			
		||||
            {
 | 
			
		||||
@@ -47,77 +47,64 @@
 | 
			
		||||
        },
 | 
			
		||||
        "identify": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:70b638cf4743f33042bebb3b51e25261a0a10e80f978739f17e7fd4837664a66",
 | 
			
		||||
                "sha256:9dfb63a2e871b807e3ba62f029813552a24b5289504f5b071dea9b041aee9fe4"
 | 
			
		||||
                "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6",
 | 
			
		||||
                "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
 | 
			
		||||
            "version": "==1.5.13"
 | 
			
		||||
            "version": "==1.4.28"
 | 
			
		||||
        },
 | 
			
		||||
        "nodeenv": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9",
 | 
			
		||||
                "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"
 | 
			
		||||
                "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.5.0"
 | 
			
		||||
            "version": "==1.4.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pre-commit": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e",
 | 
			
		||||
                "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a"
 | 
			
		||||
                "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915",
 | 
			
		||||
                "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.10.1"
 | 
			
		||||
            "version": "==2.6.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pyyaml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
 | 
			
		||||
                "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
 | 
			
		||||
                "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
 | 
			
		||||
                "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
 | 
			
		||||
                "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
 | 
			
		||||
                "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
 | 
			
		||||
                "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
 | 
			
		||||
                "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
 | 
			
		||||
                "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
 | 
			
		||||
                "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
 | 
			
		||||
                "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
 | 
			
		||||
                "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
 | 
			
		||||
                "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
 | 
			
		||||
                "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
 | 
			
		||||
                "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
 | 
			
		||||
                "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
 | 
			
		||||
                "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
 | 
			
		||||
                "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
 | 
			
		||||
                "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
 | 
			
		||||
                "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
 | 
			
		||||
                "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"
 | 
			
		||||
                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
 | 
			
		||||
                "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
 | 
			
		||||
                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
 | 
			
		||||
                "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
 | 
			
		||||
                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
 | 
			
		||||
                "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
 | 
			
		||||
                "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
 | 
			
		||||
                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
 | 
			
		||||
                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
 | 
			
		||||
                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
 | 
			
		||||
                "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
 | 
			
		||||
            "version": "==5.4.1"
 | 
			
		||||
            "version": "==5.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "six": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
 | 
			
		||||
                "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
 | 
			
		||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
 | 
			
		||||
            "version": "==1.15.0"
 | 
			
		||||
        },
 | 
			
		||||
        "toml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
 | 
			
		||||
                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
 | 
			
		||||
                "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
 | 
			
		||||
                "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
 | 
			
		||||
            "version": "==0.10.2"
 | 
			
		||||
            "version": "==0.10.1"
 | 
			
		||||
        },
 | 
			
		||||
        "virtualenv": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d",
 | 
			
		||||
                "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3"
 | 
			
		||||
                "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc",
 | 
			
		||||
                "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
 | 
			
		||||
            "version": "==20.4.2"
 | 
			
		||||
            "version": "==20.0.31"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "develop": {}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								README.md
									
									
									
									
									
								
							@@ -18,34 +18,27 @@
 | 
			
		||||
    <img alt="Docker pulls" src="https://img.shields.io/docker/pulls/taskcafe/taskcafe" />
 | 
			
		||||
  </a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
  <p align="center">
 | 
			
		||||
    <a href="https://github.com/JordanKnott/taskcafe/issues/new?assignees=&labels=&template=bug_report.md&title=">Report Bug</a>
 | 
			
		||||
    ·
 | 
			
		||||
    <a href="https://github.com/JordanKnott/taskcafe/discussions/new?category=ideas">Request Feature</a>
 | 
			
		||||
     ·
 | 
			
		||||
    <a href="https://github.com/JordanKnott/taskcafe/discussions/new?category=q-a">Ask a Question</a>
 | 
			
		||||
  </p>
 | 
			
		||||
<p align="center">
 | 
			
		||||
Was this project useful? Please consider <a href="https://www.buymeacoffee.com/jordanknott">donating</a> to help me improve it!
 | 
			
		||||
</p>
 | 
			
		||||
<p align="center">
 | 
			
		||||
 This project is still in <strong>alpha development</strong></p>
 | 
			
		||||
 | 
			
		||||
**Please note that this project is still in active development. Some options may not work yet! For updates on development, join the Discord server**
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
The following features have been implemented:
 | 
			
		||||
Currently Taskcafe only offers basic task tracking through a Kanban board.
 | 
			
		||||
 | 
			
		||||
- Manage tasks through a Kanban board interface (set due dates, labels, add checklists)
 | 
			
		||||
- View all your current assigned tasks through the My Tasks view
 | 
			
		||||
- Personal projects
 | 
			
		||||
- Task comments and activity
 | 
			
		||||
Currently you can do the following to tasks:
 | 
			
		||||
 | 
			
		||||
This project is still in active development, so some options may not be fully implemented yet.
 | 
			
		||||
 | 
			
		||||
**For updates on development, join the [Discord server](https://discord.gg/JkQDruh).**
 | 
			
		||||
- Task sorting & filtering
 | 
			
		||||
- Add colors & named labels
 | 
			
		||||
- Add due dates
 | 
			
		||||
- Descriptions written in Markdown
 | 
			
		||||
- Assign members
 | 
			
		||||
- Checklists
 | 
			
		||||
- Mark tasks as complete
 | 
			
		||||
 | 
			
		||||
For a list of planned features, check out the [Roadmap](https://github.com/JordanKnott/taskcafe/wiki/Roadmap)!
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,8 @@ user = 'taskcafe'
 | 
			
		||||
password = 'taskcafe_test'
 | 
			
		||||
 | 
			
		||||
[smtp]
 | 
			
		||||
username = 'taskcafe@example.com'
 | 
			
		||||
password = ''
 | 
			
		||||
from = 'no-reply@taskcafe.com'
 | 
			
		||||
host = 'localhost'
 | 
			
		||||
port = 11500
 | 
			
		||||
skip_verify = false
 | 
			
		||||
username = 'admin@example.com'
 | 
			
		||||
password = 'example'
 | 
			
		||||
server = 'mail.example.com'
 | 
			
		||||
port = 465
 | 
			
		||||
connection_security = 'STARTTLS'
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,6 @@
 | 
			
		||||
    "@types/axios": "^0.14.0",
 | 
			
		||||
    "@types/color": "^3.0.1",
 | 
			
		||||
    "@types/date-fns": "^2.6.0",
 | 
			
		||||
    "@types/dompurify": "^2.0.4",
 | 
			
		||||
    "@types/emoji-mart": "^3.0.4",
 | 
			
		||||
    "@types/jest": "^24.0.0",
 | 
			
		||||
    "@types/jwt-decode": "^2.2.1",
 | 
			
		||||
    "@types/lodash": "^4.14.149",
 | 
			
		||||
@@ -32,21 +30,17 @@
 | 
			
		||||
    "apollo-link-http": "^1.5.16",
 | 
			
		||||
    "apollo-link-state": "^0.4.2",
 | 
			
		||||
    "apollo-utilities": "^1.3.3",
 | 
			
		||||
    "axios": "^0.21.1",
 | 
			
		||||
    "axios": "^0.19.2",
 | 
			
		||||
    "axios-auth-refresh": "^2.2.7",
 | 
			
		||||
    "color": "^3.1.2",
 | 
			
		||||
    "date-fns": "^2.14.0",
 | 
			
		||||
    "dayjs": "^1.9.1",
 | 
			
		||||
    "dompurify": "^2.2.6",
 | 
			
		||||
    "emoji-mart": "^3.0.0",
 | 
			
		||||
    "emoticon": "^3.2.0",
 | 
			
		||||
    "graphql": "^15.0.0",
 | 
			
		||||
    "graphql-tag": "^2.10.3",
 | 
			
		||||
    "history": "^4.10.1",
 | 
			
		||||
    "immer": "^8.0.1",
 | 
			
		||||
    "immer": "^6.0.3",
 | 
			
		||||
    "jwt-decode": "^2.2.0",
 | 
			
		||||
    "lodash": "^4.17.20",
 | 
			
		||||
    "node-emoji": "^1.10.0",
 | 
			
		||||
    "prop-types": "^15.7.2",
 | 
			
		||||
    "query-string": "^6.13.7",
 | 
			
		||||
    "react": "^16.12.0",
 | 
			
		||||
@@ -54,7 +48,6 @@
 | 
			
		||||
    "react-beautiful-dnd": "^13.0.0",
 | 
			
		||||
    "react-datepicker": "^2.14.1",
 | 
			
		||||
    "react-dom": "^16.12.0",
 | 
			
		||||
    "react-emoji-render": "^1.2.4",
 | 
			
		||||
    "react-hook-form": "^6.0.6",
 | 
			
		||||
    "react-markdown": "^4.3.1",
 | 
			
		||||
    "react-router": "^5.1.2",
 | 
			
		||||
@@ -73,6 +66,8 @@
 | 
			
		||||
    "build": "react-scripts build",
 | 
			
		||||
    "test": "react-scripts test",
 | 
			
		||||
    "eject": "react-scripts eject",
 | 
			
		||||
    "storybook": "start-storybook -p 9009 -s public",
 | 
			
		||||
    "build-storybook": "build-storybook -s public",
 | 
			
		||||
    "generate": "graphql-codegen",
 | 
			
		||||
    "lint": "eslint --ext js,ts,tsx src",
 | 
			
		||||
    "tsc": "tsc"
 | 
			
		||||
@@ -97,6 +92,16 @@
 | 
			
		||||
    "@graphql-codegen/typescript": "^1.13.2",
 | 
			
		||||
    "@graphql-codegen/typescript-operations": "^1.13.2",
 | 
			
		||||
    "@graphql-codegen/typescript-react-apollo": "^1.13.2",
 | 
			
		||||
    "@storybook/addon-actions": "^5.3.13",
 | 
			
		||||
    "@storybook/addon-backgrounds": "^5.3.17",
 | 
			
		||||
    "@storybook/addon-docs": "^5.3.17",
 | 
			
		||||
    "@storybook/addon-knobs": "^5.3.17",
 | 
			
		||||
    "@storybook/addon-links": "^5.3.13",
 | 
			
		||||
    "@storybook/addon-storysource": "^5.3.17",
 | 
			
		||||
    "@storybook/addon-viewport": "^5.3.17",
 | 
			
		||||
    "@storybook/addons": "^5.3.13",
 | 
			
		||||
    "@storybook/preset-create-react-app": "^1.5.2",
 | 
			
		||||
    "@storybook/react": "^5.3.13",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^2.20.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^2.20.0",
 | 
			
		||||
    "eslint": "^6.8.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -174,7 +174,7 @@ const AdminRoute = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.title = 'Admin | Taskcafé';
 | 
			
		||||
  }, []);
 | 
			
		||||
  const { loading, data } = useUsersQuery({ fetchPolicy: 'cache-and-network' });
 | 
			
		||||
  const { loading, data } = useUsersQuery();
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const { user } = useCurrentUser();
 | 
			
		||||
  const [deleteInvitedUser] = useDeleteInvitedUserAccountMutation({
 | 
			
		||||
@@ -182,7 +182,7 @@ const AdminRoute = () => {
 | 
			
		||||
      updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
 | 
			
		||||
        produce(cache, draftCache => {
 | 
			
		||||
          draftCache.invitedUsers = cache.invitedUsers.filter(
 | 
			
		||||
            u => u.id !== response.data?.deleteInvitedUserAccount.invitedUser.id,
 | 
			
		||||
            u => u.id !== response.data.deleteInvitedUserAccount.invitedUser.id,
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
@@ -192,7 +192,7 @@ const AdminRoute = () => {
 | 
			
		||||
    update: (client, response) => {
 | 
			
		||||
      updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
 | 
			
		||||
        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);
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
@@ -203,7 +203,7 @@ const AdminRoute = () => {
 | 
			
		||||
        query: UsersDocument,
 | 
			
		||||
      });
 | 
			
		||||
      const newData = produce(cacheData, (draftState: any) => {
 | 
			
		||||
        draftState.users = [...draftState.users, { ...createData.data?.createUserAccount }];
 | 
			
		||||
        draftState.users = [...draftState.users, { ...createData.data.createUserAccount }];
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      client.writeQuery({
 | 
			
		||||
@@ -214,6 +214,9 @@ const AdminRoute = () => {
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />;
 | 
			
		||||
  }
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
    if (user.roles.org !== 'admin') {
 | 
			
		||||
      return <Redirect to="/" />;
 | 
			
		||||
@@ -256,7 +259,7 @@ const AdminRoute = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <GlobalTopNavbar projectID={null} onSaveProjectName={NOOP} name={null} />;
 | 
			
		||||
  return <span>error</span>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AdminRoute;
 | 
			
		||||
 
 | 
			
		||||
@@ -126,8 +126,4 @@ export default createGlobalStyle`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ${mixin.placeholderColor(color.textLight)}
 | 
			
		||||
 | 
			
		||||
  .picker-hidden {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,239 +0,0 @@
 | 
			
		||||
import React, { useState, useRef, useEffect } from 'react';
 | 
			
		||||
import styled from 'styled-components/macro';
 | 
			
		||||
import { useGetProjectsQuery } from 'shared/generated/graphql';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import LoadingSpinner from 'shared/components/LoadingSpinner';
 | 
			
		||||
import theme from './ThemeStyles';
 | 
			
		||||
import ControlledInput from 'shared/components/ControlledInput';
 | 
			
		||||
import { CaretDown, CaretRight } from 'shared/icons';
 | 
			
		||||
import useStickyState from 'shared/hooks/useStickyState';
 | 
			
		||||
import { usePopup } from 'shared/components/PopupMenu';
 | 
			
		||||
 | 
			
		||||
const colors = [theme.colors.primary, theme.colors.secondary];
 | 
			
		||||
 | 
			
		||||
const TeamContainer = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  margin: 0 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamTitle = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamTitleText = styled.span`
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
`;
 | 
			
		||||
const TeamProjects = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
  margin-bottom: 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectLink = styled(Link)`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectBackground = styled.div<{ idx: number }>`
 | 
			
		||||
  background-image: url(null);
 | 
			
		||||
  background-color: ${props => props.theme.colors.multiColors[props.idx % 5]};
 | 
			
		||||
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  background-position: 50%;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  &:before {
 | 
			
		||||
    background: ${props => props.theme.colors.bg.secondary};
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    content: '';
 | 
			
		||||
    left: 0;
 | 
			
		||||
    opacity: 0.88;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
const Empty = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectAvatar = styled.div<{ idx: number }>`
 | 
			
		||||
  background-image: url(null);
 | 
			
		||||
  background-color: ${props => props.theme.colors.multiColors[props.idx % 5]};
 | 
			
		||||
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  border-radius: 3px 0 0 3px;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  width: 36px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  opacity: 0.7;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectContent = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 9px 0 9px 10px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectTitle = styled.div`
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding-right: 12px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectContainer = styled.div`
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin: 0 4px 4px 0;
 | 
			
		||||
  min-width: 0;
 | 
			
		||||
  &:hover ${TeamProjectTitle} {
 | 
			
		||||
    color: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
  &:hover ${TeamProjectAvatar} {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  &:hover ${TeamProjectBackground}:before {
 | 
			
		||||
    opacity: 0.78;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
const Search = styled(ControlledInput)`
 | 
			
		||||
  margin: 0 4px 4px 4px;
 | 
			
		||||
  & input {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const Minify = styled.div`
 | 
			
		||||
  height: 28px;
 | 
			
		||||
  min-height: 28px;
 | 
			
		||||
  min-width: 28px;
 | 
			
		||||
  width: 28px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  transition-duration: 0.2s;
 | 
			
		||||
  transition-property: background, border, box-shadow, fill;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.primary};
 | 
			
		||||
    transition-duration: 0.2s;
 | 
			
		||||
    transition-property: background, border, box-shadow, fill;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectFinder = () => {
 | 
			
		||||
  const { data } = useGetProjectsQuery({ fetchPolicy: 'cache-and-network' });
 | 
			
		||||
  const [search, setSearch] = useState('');
 | 
			
		||||
  const [minified, setMinified] = useStickyState<Array<string>>([], 'project_finder_minified');
 | 
			
		||||
  const { hidePopup } = usePopup();
 | 
			
		||||
  if (data) {
 | 
			
		||||
    const { teams } = data;
 | 
			
		||||
    const projects = data.projects.filter(p => {
 | 
			
		||||
      if (search.trim() === '') return true;
 | 
			
		||||
      return p.name.toLowerCase().startsWith(search.trim().toLowerCase());
 | 
			
		||||
    });
 | 
			
		||||
    const personalProjects = projects.filter(p => p.team === null);
 | 
			
		||||
    const projectTeams = [
 | 
			
		||||
      { id: 'personal', name: 'Personal', projects: personalProjects.sort((a, b) => a.name.localeCompare(b.name)) },
 | 
			
		||||
      ...teams.map(team => {
 | 
			
		||||
        return {
 | 
			
		||||
          id: team.id,
 | 
			
		||||
          name: team.name,
 | 
			
		||||
          projects: projects
 | 
			
		||||
            .filter(project => project.team && project.team.id === team.id)
 | 
			
		||||
            .sort((a, b) => a.name.localeCompare(b.name)),
 | 
			
		||||
        };
 | 
			
		||||
      }),
 | 
			
		||||
    ];
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <Search
 | 
			
		||||
          autoFocus
 | 
			
		||||
          variant="alternate"
 | 
			
		||||
          value={search}
 | 
			
		||||
          onChange={e => setSearch(e.currentTarget.value)}
 | 
			
		||||
          placeholder="Find projects by name..."
 | 
			
		||||
        />
 | 
			
		||||
        {projectTeams.map(team => {
 | 
			
		||||
          const isMinified = minified.find(m => m === team.id);
 | 
			
		||||
          if (team.projects.length === 0) return null;
 | 
			
		||||
          return (
 | 
			
		||||
            <TeamContainer key={team.id}>
 | 
			
		||||
              <TeamTitle>
 | 
			
		||||
                <TeamTitleText>{team.name}</TeamTitleText>
 | 
			
		||||
                {isMinified ? (
 | 
			
		||||
                  <Minify onClick={() => setMinified(prev => prev.filter(m => m !== team.id))}>
 | 
			
		||||
                    <CaretRight width={16} height={16} />
 | 
			
		||||
                  </Minify>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <Minify onClick={() => setMinified(prev => [...prev, team.id])}>
 | 
			
		||||
                    <CaretDown width={16} height={16} />
 | 
			
		||||
                  </Minify>
 | 
			
		||||
                )}
 | 
			
		||||
              </TeamTitle>
 | 
			
		||||
              {!isMinified && (
 | 
			
		||||
                <TeamProjects>
 | 
			
		||||
                  {team.projects.map((project, idx) => (
 | 
			
		||||
                    <TeamProjectContainer key={project.id}>
 | 
			
		||||
                      <TeamProjectLink onClick={e => hidePopup()} to={`/projects/${project.id}`}>
 | 
			
		||||
                        <TeamProjectBackground idx={idx} />
 | 
			
		||||
                        <TeamProjectAvatar idx={idx} />
 | 
			
		||||
                        <TeamProjectContent>
 | 
			
		||||
                          <TeamProjectTitle>{project.name}</TeamProjectTitle>
 | 
			
		||||
                        </TeamProjectContent>
 | 
			
		||||
                      </TeamProjectLink>
 | 
			
		||||
                    </TeamProjectContainer>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </TeamProjects>
 | 
			
		||||
              )}
 | 
			
		||||
            </TeamContainer>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Empty>
 | 
			
		||||
      <LoadingSpinner />
 | 
			
		||||
    </Empty>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ProjectFinder;
 | 
			
		||||
@@ -4,7 +4,6 @@ import * as H from 'history';
 | 
			
		||||
 | 
			
		||||
import Dashboard from 'Dashboard';
 | 
			
		||||
import Admin from 'Admin';
 | 
			
		||||
import MyTasks from 'MyTasks';
 | 
			
		||||
import Confirm from 'Confirm';
 | 
			
		||||
import Projects from 'Projects';
 | 
			
		||||
import Project from 'Projects/Project';
 | 
			
		||||
@@ -26,11 +25,6 @@ const MainContent = styled.div`
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type RefreshTokenResponse = {
 | 
			
		||||
  accessToken: string;
 | 
			
		||||
  setup?: null | { confirmToken: string };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AuthorizedRoutes = () => {
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
@@ -70,7 +64,6 @@ const AuthorizedRoutes = () => {
 | 
			
		||||
        <Route path="/teams/:teamID" component={Teams} />
 | 
			
		||||
        <Route path="/profile" component={Profile} />
 | 
			
		||||
        <Route path="/admin" component={Admin} />
 | 
			
		||||
        <Route path="/tasks" component={MyTasks} />
 | 
			
		||||
      </MainContent>
 | 
			
		||||
    </Switch>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
 | 
			
		||||
import styled from 'styled-components/macro';
 | 
			
		||||
import { ProfileMenu } from 'shared/components/DropdownMenu';
 | 
			
		||||
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
 | 
			
		||||
import { useHistory } from 'react-router';
 | 
			
		||||
@@ -8,18 +9,165 @@ import {
 | 
			
		||||
  RoleCode,
 | 
			
		||||
  useTopNavbarQuery,
 | 
			
		||||
  useDeleteProjectMutation,
 | 
			
		||||
  useGetProjectsQuery,
 | 
			
		||||
  GetProjectsDocument,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
 | 
			
		||||
import { History } from 'history';
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import MiniProfile from 'shared/components/MiniProfile';
 | 
			
		||||
import cache from 'App/cache';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import NotificationPopup, { NotificationItem } from 'shared/components/NotifcationPopup';
 | 
			
		||||
import theme from './ThemeStyles';
 | 
			
		||||
import ProjectFinder from './ProjectFinder';
 | 
			
		||||
 | 
			
		||||
const TeamContainer = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  margin: 0 8px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamTitle = styled.h3`
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjects = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
  margin-bottom: 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectLink = styled(Link)`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectBackground = styled.div<{ color: string }>`
 | 
			
		||||
  background-image: url(null);
 | 
			
		||||
  background-color: ${props => props.color};
 | 
			
		||||
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  background-position: 50%;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  &:before {
 | 
			
		||||
    background: ${props => props.theme.colors.bg.secondary};
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    content: '';
 | 
			
		||||
    left: 0;
 | 
			
		||||
    opacity: 0.88;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectAvatar = styled.div<{ color: string }>`
 | 
			
		||||
  background-image: url(null);
 | 
			
		||||
  background-color: ${props => props.color};
 | 
			
		||||
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  background-size: cover;
 | 
			
		||||
  border-radius: 3px 0 0 3px;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  width: 36px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  opacity: 0.7;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectContent = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 9px 0 9px 10px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectTitle = styled.div`
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding-right: 12px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TeamProjectContainer = styled.div`
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin: 0 4px 4px 0;
 | 
			
		||||
  min-width: 0;
 | 
			
		||||
  &:hover ${TeamProjectTitle} {
 | 
			
		||||
    color: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
  &:hover ${TeamProjectAvatar} {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  &:hover ${TeamProjectBackground}:before {
 | 
			
		||||
    opacity: 0.78;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const colors = [theme.colors.primary, theme.colors.secondary];
 | 
			
		||||
 | 
			
		||||
const ProjectFinder = () => {
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery();
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <span>loading</span>;
 | 
			
		||||
  }
 | 
			
		||||
  if (data) {
 | 
			
		||||
    const { projects, teams } = data;
 | 
			
		||||
    const projectTeams = teams.map(team => {
 | 
			
		||||
      return {
 | 
			
		||||
        id: team.id,
 | 
			
		||||
        name: team.name,
 | 
			
		||||
        projects: projects.filter(project => project.team && project.team.id === team.id),
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {projectTeams.map(team => (
 | 
			
		||||
          <TeamContainer key={team.id}>
 | 
			
		||||
            <TeamTitle>{team.name}</TeamTitle>
 | 
			
		||||
            <TeamProjects>
 | 
			
		||||
              {team.projects.map((project, idx) => (
 | 
			
		||||
                <TeamProjectContainer key={project.id}>
 | 
			
		||||
                  <TeamProjectLink to={`/projects/${project.id}`}>
 | 
			
		||||
                    <TeamProjectBackground color={colors[idx % 5]} />
 | 
			
		||||
                    <TeamProjectAvatar color={colors[idx % 5]} />
 | 
			
		||||
                    <TeamProjectContent>
 | 
			
		||||
                      <TeamProjectTitle>{project.name}</TeamProjectTitle>
 | 
			
		||||
                    </TeamProjectContent>
 | 
			
		||||
                  </TeamProjectLink>
 | 
			
		||||
                </TeamProjectContainer>
 | 
			
		||||
              ))}
 | 
			
		||||
            </TeamProjects>
 | 
			
		||||
          </TeamContainer>
 | 
			
		||||
        ))}
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <span>error</span>;
 | 
			
		||||
};
 | 
			
		||||
type ProjectPopupProps = {
 | 
			
		||||
  history: any;
 | 
			
		||||
  history: History<History.PoorMansUnknown>;
 | 
			
		||||
  name: string;
 | 
			
		||||
  projectID: string;
 | 
			
		||||
};
 | 
			
		||||
@@ -34,7 +182,7 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
 | 
			
		||||
 | 
			
		||||
      const newData = produce(cacheData, (draftState: any) => {
 | 
			
		||||
        draftState.projects = draftState.projects.filter(
 | 
			
		||||
          (project: any) => project.id !== deleteData.data?.deleteProject.project.id,
 | 
			
		||||
          (project: any) => project.id !== deleteData.data.deleteProject.project.id,
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@@ -277,9 +425,6 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
 | 
			
		||||
        onDashboardClick={() => {
 | 
			
		||||
          history.push('/');
 | 
			
		||||
        }}
 | 
			
		||||
        onMyTasksClick={() => {
 | 
			
		||||
          history.push('/tasks');
 | 
			
		||||
        }}
 | 
			
		||||
        projectMembers={projectMembers}
 | 
			
		||||
        projectInvitedMembers={projectInvitedMembers}
 | 
			
		||||
        onProfileClick={onProfileClick}
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,10 @@ const StyledContainer = styled(ToastContainer).attrs({
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const history = createBrowserHistory();
 | 
			
		||||
type RefreshTokenResponse = {
 | 
			
		||||
  accessToken: string;
 | 
			
		||||
  isInstalled: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const App = () => {
 | 
			
		||||
  const [user, setUser] = useState<CurrentUserRaw | null>(null);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,145 +0,0 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import styled, { css } from 'styled-components';
 | 
			
		||||
import { Checkmark, User, Calendar, Tags, Clock } from 'shared/icons';
 | 
			
		||||
import { TaskMetaFilters, TaskMeta, TaskMetaMatch, DueDateFilterType } from 'shared/components/Lists';
 | 
			
		||||
import Input from 'shared/components/ControlledInput';
 | 
			
		||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import { mixin } from 'shared/utils/styles';
 | 
			
		||||
import Member from 'shared/components/Member';
 | 
			
		||||
import { MyTasksSort } from 'shared/generated/graphql';
 | 
			
		||||
 | 
			
		||||
const FilterMember = styled(Member)`
 | 
			
		||||
  margin: 2px 0;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    background: ${props => props.theme.colors.primary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Labels = styled.ul`
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  margin: 0 8px;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin-bottom: 8px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Label = styled.li`
 | 
			
		||||
  position: relative;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CardLabel = styled.span<{ active: boolean; color: string }>`
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.active &&
 | 
			
		||||
    css`
 | 
			
		||||
      margin-left: 4px;
 | 
			
		||||
      box-shadow: -8px 0 ${mixin.darken(props.color, 0.12)};
 | 
			
		||||
      border-radius: 3px;
 | 
			
		||||
    `}
 | 
			
		||||
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  margin: 0 0 4px;
 | 
			
		||||
  min-height: 20px;
 | 
			
		||||
  padding: 6px 12px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  transition: padding 85ms, margin 85ms, box-shadow 85ms;
 | 
			
		||||
  background-color: ${props => props.color};
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  display: block;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  min-height: 31px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionsList = styled.ul`
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionItem = styled.li`
 | 
			
		||||
  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};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionTitle = styled.span`
 | 
			
		||||
  margin-left: 20px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ActiveIcon = styled(Checkmark)`
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ItemIcon = styled.div`
 | 
			
		||||
  position: absolute;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskNameInput = styled(Input)`
 | 
			
		||||
  margin: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ActionItemLine = styled.div`
 | 
			
		||||
  height: 1px;
 | 
			
		||||
  border-top: 1px solid #414561;
 | 
			
		||||
  margin: 0.25rem !important;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type MyTasksSortProps = {
 | 
			
		||||
  sort: MyTasksSort;
 | 
			
		||||
  onChangeSort: (sort: MyTasksSort) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MyTasksSortPopup: React.FC<MyTasksSortProps> = ({ sort: initialSort, onChangeSort }) => {
 | 
			
		||||
  const [sort, setSort] = useState(initialSort);
 | 
			
		||||
  const handleChangeSort = (f: MyTasksSort) => {
 | 
			
		||||
    setSort(f);
 | 
			
		||||
    onChangeSort(f);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Popup tab={0} title={null}>
 | 
			
		||||
        <ActionsList>
 | 
			
		||||
          <ActionItem onClick={() => handleChangeSort(MyTasksSort.None)}>
 | 
			
		||||
            {sort === MyTasksSort.None && <ActiveIcon width={16} height={16} />}
 | 
			
		||||
            <ActionTitle>None</ActionTitle>
 | 
			
		||||
          </ActionItem>
 | 
			
		||||
          <ActionItem onClick={() => handleChangeSort(MyTasksSort.Project)}>
 | 
			
		||||
            {sort === MyTasksSort.Project && <ActiveIcon width={16} height={16} />}
 | 
			
		||||
            <ActionTitle>Project</ActionTitle>
 | 
			
		||||
          </ActionItem>
 | 
			
		||||
          <ActionItem onClick={() => handleChangeSort(MyTasksSort.DueDate)}>
 | 
			
		||||
            {sort === MyTasksSort.DueDate && <ActiveIcon width={16} height={16} />}
 | 
			
		||||
            <ActionTitle>Due Date</ActionTitle>
 | 
			
		||||
          </ActionItem>
 | 
			
		||||
        </ActionsList>
 | 
			
		||||
      </Popup>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MyTasksSortPopup;
 | 
			
		||||
@@ -1,151 +0,0 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import { Checkmark } from 'shared/icons';
 | 
			
		||||
import { TaskStatusFilter, TaskStatus, TaskSince } from 'shared/components/Lists';
 | 
			
		||||
import { MyTasksStatus } from 'shared/generated/graphql';
 | 
			
		||||
import { Popup } from 'shared/components/PopupMenu';
 | 
			
		||||
 | 
			
		||||
export const ActionsList = styled.ul`
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionExtraMenuContainer = styled.div`
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 100%;
 | 
			
		||||
  top: -4px;
 | 
			
		||||
  padding-left: 2px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionItem = styled.li`
 | 
			
		||||
  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};
 | 
			
		||||
  }
 | 
			
		||||
  &:hover ${ActionExtraMenuContainer} {
 | 
			
		||||
    visibility: visible;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionTitle = styled.span`
 | 
			
		||||
  margin-left: 20px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionExtraMenu = styled.ul`
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  padding-top: 8px;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
 | 
			
		||||
 | 
			
		||||
  color: #c2c6dc;
 | 
			
		||||
  background: #262c49;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  border-color: #414561;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionExtraMenuItem = styled.li`
 | 
			
		||||
  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 ActionExtraMenuSeparator = styled.li`
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  padding-left: 4px;
 | 
			
		||||
  padding-right: 4px;
 | 
			
		||||
  padding-top: 0.25rem;
 | 
			
		||||
  padding-bottom: 0.25rem;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ActiveIcon = styled(Checkmark)`
 | 
			
		||||
  position: absolute;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type MyTasksStatusProps = {
 | 
			
		||||
  status: MyTasksStatus;
 | 
			
		||||
  onChangeStatus: (status: MyTasksStatus) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MyTasksStatusPopup: React.FC<MyTasksStatusProps> = ({ status: initialStatus, onChangeStatus }) => {
 | 
			
		||||
  const [status, setStatus] = useState(initialStatus);
 | 
			
		||||
  const handleStatusChange = (f: MyTasksStatus) => {
 | 
			
		||||
    setStatus(f);
 | 
			
		||||
    onChangeStatus(f);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Popup tab={0} title={null}>
 | 
			
		||||
      <ActionsList>
 | 
			
		||||
        <ActionItem onClick={() => handleStatusChange(MyTasksStatus.Incomplete)}>
 | 
			
		||||
          {status === MyTasksStatus.Incomplete && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
          <ActionTitle>Incomplete Tasks</ActionTitle>
 | 
			
		||||
        </ActionItem>
 | 
			
		||||
        <ActionItem>
 | 
			
		||||
          {status !== MyTasksStatus.Incomplete && status !== MyTasksStatus.All && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
          <ActionTitle>Compelete Tasks</ActionTitle>
 | 
			
		||||
          <ActionExtraMenuContainer>
 | 
			
		||||
            <ActionExtraMenu>
 | 
			
		||||
              <ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteAll)}>
 | 
			
		||||
                {status === MyTasksStatus.CompleteAll && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
                <ActionTitle>All completed tasks</ActionTitle>
 | 
			
		||||
              </ActionExtraMenuItem>
 | 
			
		||||
              <ActionExtraMenuSeparator>Marked complete since</ActionExtraMenuSeparator>
 | 
			
		||||
              <ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteToday)}>
 | 
			
		||||
                {status === MyTasksStatus.CompleteToday && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
                <ActionTitle>Today</ActionTitle>
 | 
			
		||||
              </ActionExtraMenuItem>
 | 
			
		||||
              <ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteYesterday)}>
 | 
			
		||||
                {status === MyTasksStatus.CompleteYesterday && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
 | 
			
		||||
                <ActionTitle>Yesterday</ActionTitle>
 | 
			
		||||
              </ActionExtraMenuItem>
 | 
			
		||||
              <ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteOneWeek)}>
 | 
			
		||||
                {status === MyTasksStatus.CompleteOneWeek && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
                <ActionTitle>1 week</ActionTitle>
 | 
			
		||||
              </ActionExtraMenuItem>
 | 
			
		||||
              <ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteTwoWeek)}>
 | 
			
		||||
                {status === MyTasksStatus.CompleteTwoWeek && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
                <ActionTitle>2 weeks</ActionTitle>
 | 
			
		||||
              </ActionExtraMenuItem>
 | 
			
		||||
              <ActionExtraMenuItem onClick={() => handleStatusChange(MyTasksStatus.CompleteThreeWeek)}>
 | 
			
		||||
                {status === MyTasksStatus.CompleteThreeWeek && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
                <ActionTitle>3 weeks</ActionTitle>
 | 
			
		||||
              </ActionExtraMenuItem>
 | 
			
		||||
            </ActionExtraMenu>
 | 
			
		||||
          </ActionExtraMenuContainer>
 | 
			
		||||
        </ActionItem>
 | 
			
		||||
        <ActionItem onClick={() => handleStatusChange(MyTasksStatus.All)}>
 | 
			
		||||
          {status === MyTasksStatus.All && <ActiveIcon width={12} height={12} />}
 | 
			
		||||
          <ActionTitle>All Tasks</ActionTitle>
 | 
			
		||||
        </ActionItem>
 | 
			
		||||
      </ActionsList>
 | 
			
		||||
    </Popup>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MyTasksStatusPopup;
 | 
			
		||||
@@ -1,415 +0,0 @@
 | 
			
		||||
import React, { useState, useRef, useEffect } from 'react';
 | 
			
		||||
import styled, { css } from 'styled-components/macro';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import { CheckCircleOutline, CheckCircle, Cross, Briefcase, ChevronRight } from 'shared/icons';
 | 
			
		||||
import { mixin } from 'shared/utils/styles';
 | 
			
		||||
 | 
			
		||||
const RIGHT_ROW_WIDTH = 327;
 | 
			
		||||
const TaskName = styled.div<{ focused: boolean }>`
 | 
			
		||||
  flex: 0 1 auto;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border: 1px solid transparent;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  padding: 0 1px;
 | 
			
		||||
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    ${props =>
 | 
			
		||||
      !props.focused &&
 | 
			
		||||
      css`
 | 
			
		||||
        border-color: #9ca6af !important;
 | 
			
		||||
        border: 1px solid ${props.theme.colors.primary} !important;
 | 
			
		||||
      `}
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const DueDateCell = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  align-self: stretch;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const CellPlaceholder = styled.div<{ width: number }>`
 | 
			
		||||
  min-width: ${p => p.width}px;
 | 
			
		||||
  width: ${p => p.width}px;
 | 
			
		||||
`;
 | 
			
		||||
const DueDateCellDisplay = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const DueDateCellLabel = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
 | 
			
		||||
  font-size: 11px;
 | 
			
		||||
  flex: 0 1 auto;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-flow: row wrap;
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const DueDateRemoveButton = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  padding-left: 4px;
 | 
			
		||||
  padding-right: 8px;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
  svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  }
 | 
			
		||||
  &:hover svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
const TaskGroupItemCell = styled.div<{ width: number; focused: boolean }>`
 | 
			
		||||
  width: ${p => p.width}px;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  border: 1px solid #414561;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  margin-right: -1px;
 | 
			
		||||
  z-index: 0;
 | 
			
		||||
  padding: 0 8px;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  &:hover ${DueDateRemoveButton} {
 | 
			
		||||
    visibility: visible;
 | 
			
		||||
  }
 | 
			
		||||
  &:hover ${TaskName} {
 | 
			
		||||
    ${props =>
 | 
			
		||||
      !props.focused &&
 | 
			
		||||
      css`
 | 
			
		||||
        background: ${props.theme.colors.bg.secondary};
 | 
			
		||||
        border: 1px solid ${mixin.darken(props.theme.colors.bg.secondary, 0.25)};
 | 
			
		||||
        border-radius: 2px;
 | 
			
		||||
        cursor: text;
 | 
			
		||||
      `}
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskGroupItem = styled.div`
 | 
			
		||||
  padding-right: 24px;
 | 
			
		||||
  contain: style;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin-bottom: -1px;
 | 
			
		||||
  margin-top: -1px;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background-color: #161d31;
 | 
			
		||||
  }
 | 
			
		||||
  & ${TaskGroupItemCell}:first-child {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    padding: 0 4px 0 0;
 | 
			
		||||
    margin-left: 24px;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    min-width: 1px;
 | 
			
		||||
    border-right: 0;
 | 
			
		||||
    border-left: 0;
 | 
			
		||||
  }
 | 
			
		||||
  & ${TaskGroupItemCell}:last-child {
 | 
			
		||||
    border-right: 0;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskItemComplete = styled.div`
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  margin: 0 3px 0 0;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  svg {
 | 
			
		||||
    transition: all 0.2 ease;
 | 
			
		||||
  }
 | 
			
		||||
  &:hover svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.primary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskDetailsButton = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  padding-left: 4px;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskDetailsArea = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex: 1 0 auto;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin-right: 24px;
 | 
			
		||||
  &:hover ${TaskDetailsButton} {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskDetailsWorkpace = styled(Briefcase)`
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
`;
 | 
			
		||||
const TaskDetailsLabel = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskDetailsChevron = styled(ChevronRight)`
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskNameShadow = styled.div`
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  min-height: 1em;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  visibility: hidden;
 | 
			
		||||
  white-space: pre;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  line-height: 20px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  min-width: 20px;
 | 
			
		||||
  padding: 0 4px;
 | 
			
		||||
  text-rendering: optimizeSpeed;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskNameInput = styled.textarea`
 | 
			
		||||
  white-space: pre;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border-radius: 0;
 | 
			
		||||
  display: block;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  outline: 0;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  resize: none;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  line-height: 20px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  min-width: 20px;
 | 
			
		||||
  padding: 0 4px;
 | 
			
		||||
  text-rendering: optimizeSpeed;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPill = styled.div`
 | 
			
		||||
  background-color: ${props => props.theme.colors.bg.primary};
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  border-radius: 10px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: block;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  line-height: 20px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding: 0 8px;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPillContents = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPillName = styled.span`
 | 
			
		||||
  flex: 0 1 auto;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPillColor = styled.svg`
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
  fill: #0064fb;
 | 
			
		||||
  height: 12px;
 | 
			
		||||
  width: 12px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type TaskEntryProps = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  dueDate?: string | null;
 | 
			
		||||
  onEditName: (name: string) => void;
 | 
			
		||||
  project: string;
 | 
			
		||||
  hasTime: boolean;
 | 
			
		||||
  autoFocus?: boolean;
 | 
			
		||||
  onEditProject: ($target: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onToggleComplete: (complete: boolean) => void;
 | 
			
		||||
  complete: boolean;
 | 
			
		||||
  onEditDueDate: ($target: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onTaskDetails: () => void;
 | 
			
		||||
  onRemoveDueDate: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const TaskEntry: React.FC<TaskEntryProps> = ({
 | 
			
		||||
  autoFocus = false,
 | 
			
		||||
  onToggleComplete,
 | 
			
		||||
  onEditName,
 | 
			
		||||
  onTaskDetails,
 | 
			
		||||
  name: initialName,
 | 
			
		||||
  complete,
 | 
			
		||||
  project,
 | 
			
		||||
  dueDate,
 | 
			
		||||
  hasTime,
 | 
			
		||||
  onEditProject,
 | 
			
		||||
  onEditDueDate,
 | 
			
		||||
  onRemoveDueDate,
 | 
			
		||||
}) => {
 | 
			
		||||
  const leftRow = window.innerWidth - RIGHT_ROW_WIDTH;
 | 
			
		||||
  const [focused, setFocused] = useState(autoFocus);
 | 
			
		||||
  const [name, setName] = useState(initialName);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setName(initialName);
 | 
			
		||||
  }, [initialName]);
 | 
			
		||||
  const $projects = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $dueDate = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $nameInput = useRef<HTMLTextAreaElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <TaskGroupItem>
 | 
			
		||||
      <TaskGroupItemCell focused={focused} width={leftRow}>
 | 
			
		||||
        <TaskItemComplete onClick={() => onToggleComplete(!complete)}>
 | 
			
		||||
          {complete ? <CheckCircle width={16} height={16} /> : <CheckCircleOutline width={16} height={16} />}
 | 
			
		||||
        </TaskItemComplete>
 | 
			
		||||
        <TaskName focused={focused}>
 | 
			
		||||
          <TaskNameShadow>{name}</TaskNameShadow>
 | 
			
		||||
          <TaskNameInput
 | 
			
		||||
            autoFocus={autoFocus}
 | 
			
		||||
            onFocus={() => setFocused(true)}
 | 
			
		||||
            ref={$nameInput}
 | 
			
		||||
            onBlur={() => {
 | 
			
		||||
              setFocused(false);
 | 
			
		||||
              onEditName(name);
 | 
			
		||||
            }}
 | 
			
		||||
            onKeyDown={e => {
 | 
			
		||||
              if (e.keyCode === 13) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                if ($nameInput.current) {
 | 
			
		||||
                  $nameInput.current.blur();
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            onChange={e => setName(e.currentTarget.value)}
 | 
			
		||||
            wrap="off"
 | 
			
		||||
            value={name}
 | 
			
		||||
            rows={1}
 | 
			
		||||
          />
 | 
			
		||||
        </TaskName>
 | 
			
		||||
        <TaskDetailsArea onClick={() => onTaskDetails()}>
 | 
			
		||||
          <TaskDetailsButton>
 | 
			
		||||
            <TaskDetailsWorkpace width={16} height={16} />
 | 
			
		||||
            <TaskDetailsLabel>
 | 
			
		||||
              Details
 | 
			
		||||
              <TaskDetailsChevron width={12} height={12} />
 | 
			
		||||
            </TaskDetailsLabel>
 | 
			
		||||
          </TaskDetailsButton>
 | 
			
		||||
        </TaskDetailsArea>
 | 
			
		||||
      </TaskGroupItemCell>
 | 
			
		||||
      <CellPlaceholder width={leftRow} />
 | 
			
		||||
      <TaskGroupItemCell width={120} focused={false} ref={$dueDate}>
 | 
			
		||||
        <DueDateCell onClick={() => onEditDueDate($dueDate)}>
 | 
			
		||||
          <DueDateCellDisplay>
 | 
			
		||||
            <DueDateCellLabel>
 | 
			
		||||
              {dueDate ? dayjs(dueDate).format(hasTime ? 'MMM D [at] h:mm A' : 'MMM D') : ''}
 | 
			
		||||
            </DueDateCellLabel>
 | 
			
		||||
          </DueDateCellDisplay>
 | 
			
		||||
        </DueDateCell>
 | 
			
		||||
        {dueDate && (
 | 
			
		||||
          <DueDateRemoveButton onClick={() => onRemoveDueDate()}>
 | 
			
		||||
            <Cross width={12} height={12} />
 | 
			
		||||
          </DueDateRemoveButton>
 | 
			
		||||
        )}
 | 
			
		||||
      </TaskGroupItemCell>
 | 
			
		||||
      <TaskGroupItemCell width={120} focused={false} ref={$projects}>
 | 
			
		||||
        <ProjectPill
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            onEditProject($projects);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <ProjectPillContents>
 | 
			
		||||
            <ProjectPillColor viewBox="0 0 24 24" focusable={false}>
 | 
			
		||||
              <path d="M10.4,4h3.2c2.2,0,3,0.2,3.9,0.7c0.8,0.4,1.5,1.1,1.9,1.9s0.7,1.6,0.7,3.9v3.2c0,2.2-0.2,3-0.7,3.9c-0.4,0.8-1.1,1.5-1.9,1.9s-1.6,0.7-3.9,0.7h-3.2c-2.2,0-3-0.2-3.9-0.7c-0.8-0.4-1.5-1.1-1.9-1.9c-0.4-1-0.6-1.8-0.6-4v-3.2c0-2.2,0.2-3,0.7-3.9C5.1,5.7,5.8,5,6.6,4.6C7.4,4.2,8.2,4,10.4,4z" />
 | 
			
		||||
            </ProjectPillColor>
 | 
			
		||||
            <ProjectPillName>{project}</ProjectPillName>
 | 
			
		||||
          </ProjectPillContents>
 | 
			
		||||
        </ProjectPill>
 | 
			
		||||
      </TaskGroupItemCell>
 | 
			
		||||
      <TaskGroupItemCell width={50} focused={false} />
 | 
			
		||||
    </TaskGroupItem>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
export default TaskEntry;
 | 
			
		||||
type NewTaskEntryProps = {
 | 
			
		||||
  onClick: () => void;
 | 
			
		||||
};
 | 
			
		||||
const AddTaskLabel = styled.span`
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  z-index: 0;
 | 
			
		||||
  padding: 0 8px;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  flex: 1 1;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  margin-left: 24px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const NewTaskEntry: React.FC<NewTaskEntryProps> = ({ onClick }) => {
 | 
			
		||||
  const leftRow = window.innerWidth - RIGHT_ROW_WIDTH;
 | 
			
		||||
  return (
 | 
			
		||||
    <TaskGroupItem>
 | 
			
		||||
      <AddTaskLabel onClick={onClick}>Add task...</AddTaskLabel>
 | 
			
		||||
    </TaskGroupItem>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { NewTaskEntry };
 | 
			
		||||
@@ -1,913 +0,0 @@
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import styled, { css } from 'styled-components/macro';
 | 
			
		||||
import GlobalTopNavbar from 'App/TopNavbar';
 | 
			
		||||
import Details from 'Projects/Project/Details';
 | 
			
		||||
import {
 | 
			
		||||
  useMyTasksQuery,
 | 
			
		||||
  MyTasksSort,
 | 
			
		||||
  MyTasksStatus,
 | 
			
		||||
  useCreateTaskMutation,
 | 
			
		||||
  MyTasksQuery,
 | 
			
		||||
  MyTasksDocument,
 | 
			
		||||
  useUpdateTaskNameMutation,
 | 
			
		||||
  useSetTaskCompleteMutation,
 | 
			
		||||
  useUpdateTaskDueDateMutation,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
 | 
			
		||||
import { Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
 | 
			
		||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
 | 
			
		||||
import updateApolloCache from 'shared/utils/cache';
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import { Sort, Cogs, CaretDown, CheckCircle, CaretRight, CheckCircleOutline } from 'shared/icons';
 | 
			
		||||
import Select from 'react-select';
 | 
			
		||||
import { editorColourStyles } from 'shared/components/Select';
 | 
			
		||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
			
		||||
import DueDateManager from 'shared/components/DueDateManager';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import useStickyState from 'shared/hooks/useStickyState';
 | 
			
		||||
import MyTasksSortPopup from './MyTasksSort';
 | 
			
		||||
import MyTasksStatusPopup from './MyTasksStatus';
 | 
			
		||||
import TaskEntry from './TaskEntry';
 | 
			
		||||
 | 
			
		||||
type TaskRouteProps = {
 | 
			
		||||
  taskID: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function prettyStatus(status: MyTasksStatus) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case MyTasksStatus.All:
 | 
			
		||||
      return 'All tasks';
 | 
			
		||||
    case MyTasksStatus.Incomplete:
 | 
			
		||||
      return 'Incomplete tasks';
 | 
			
		||||
    case MyTasksStatus.CompleteAll:
 | 
			
		||||
      return 'All completed tasks';
 | 
			
		||||
    case MyTasksStatus.CompleteToday:
 | 
			
		||||
      return 'Completed tasks: today';
 | 
			
		||||
    case MyTasksStatus.CompleteYesterday:
 | 
			
		||||
      return 'Completed tasks: yesterday';
 | 
			
		||||
    case MyTasksStatus.CompleteOneWeek:
 | 
			
		||||
      return 'Completed tasks: 1 week';
 | 
			
		||||
    case MyTasksStatus.CompleteTwoWeek:
 | 
			
		||||
      return 'Completed tasks: 2 weeks';
 | 
			
		||||
    case MyTasksStatus.CompleteThreeWeek:
 | 
			
		||||
      return 'Completed tasks: 3 weeks';
 | 
			
		||||
    default:
 | 
			
		||||
      return 'unknown tasks';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function prettySort(sort: MyTasksSort) {
 | 
			
		||||
  if (sort === MyTasksSort.None) {
 | 
			
		||||
    return 'Sort';
 | 
			
		||||
  }
 | 
			
		||||
  return `Sort: ${sort.charAt(0) +
 | 
			
		||||
    sort
 | 
			
		||||
      .slice(1)
 | 
			
		||||
      .toLowerCase()
 | 
			
		||||
      .replace(/_/gi, ' ')}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Group = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  name: string | null;
 | 
			
		||||
  tasks: Array<Task>;
 | 
			
		||||
};
 | 
			
		||||
const DueDateEditorLabel = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
 | 
			
		||||
  font-size: 11px;
 | 
			
		||||
  padding: 0 8px;
 | 
			
		||||
  flex: 0 1 auto;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-flow: row wrap;
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  height: 35px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectBar = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  height: 40px;
 | 
			
		||||
  padding: 0 12px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectActions = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectActionWrapper = styled.div<{ disabled?: boolean }>`
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
 | 
			
		||||
  &:not(:last-of-type) {
 | 
			
		||||
    margin-right: 16px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.disabled &&
 | 
			
		||||
    css`
 | 
			
		||||
      opacity: 0.5;
 | 
			
		||||
      cursor: default;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
    `}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectActionText = styled.span`
 | 
			
		||||
  padding-left: 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type ProjectActionProps = {
 | 
			
		||||
  onClick?: (target: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ProjectAction: React.FC<ProjectActionProps> = ({ onClick, disabled = false, children }) => {
 | 
			
		||||
  const $container = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const handleClick = () => {
 | 
			
		||||
    if (onClick) {
 | 
			
		||||
      onClick($container);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <ProjectActionWrapper ref={$container} onClick={handleClick} disabled={disabled}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </ProjectActionWrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const EditorPositioner = styled.div<{ top: number; left: number }>`
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: ${p => p.top}px;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  margin-left: -100vw;
 | 
			
		||||
  z-index: 10000;
 | 
			
		||||
  align-items: flex-start;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  height: 0;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  width: 100vw;
 | 
			
		||||
  left: ${p => p.left}px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const EditorPositionerContents = styled.div`
 | 
			
		||||
  position: relative;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const EditorContainer = styled.div<{ width: number }>`
 | 
			
		||||
  border: 1px solid ${props => props.theme.colors.primary};
 | 
			
		||||
  background: ${props => props.theme.colors.bg.secondary};
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: ${p => p.width}px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const EditorCell = styled.div<{ width: number }>`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  width: ${p => p.width}px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// TABLE
 | 
			
		||||
const VerticalScoller = styled.div`
 | 
			
		||||
  contain: strict;
 | 
			
		||||
  flex: 1 1 auto;
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
  padding-bottom: 1px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  min-height: 1px;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const VerticalScollerInner = styled.div`
 | 
			
		||||
  min-height: 100%;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const VerticalScollerInnerBar = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin: 0 24px;
 | 
			
		||||
  margin-bottom: 1px;
 | 
			
		||||
  border-top: 1px solid #414561;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TableContents = styled.div`
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  margin-bottom: 32px;
 | 
			
		||||
  min-width: 100%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskGroupContainer = styled.div``;
 | 
			
		||||
 | 
			
		||||
const TaskGroupHeader = styled.div`
 | 
			
		||||
  height: 50px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskGroupItems = styled.div`
 | 
			
		||||
  overflow: unset;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPill = styled.div`
 | 
			
		||||
  background-color: ${props => props.theme.colors.bg.primary};
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  border-radius: 10px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: block;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  line-height: 20px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding: 0 8px;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPillContents = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPillName = styled.span`
 | 
			
		||||
  flex: 0 1 auto;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectPillColor = styled.svg`
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
  fill: #0064fb;
 | 
			
		||||
  height: 12px;
 | 
			
		||||
  width: 12px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const SingleValue = ({ children, ...props }: any) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <ProjectPill>
 | 
			
		||||
      <ProjectPillContents>
 | 
			
		||||
        <ProjectPillColor viewBox="0 0 24 24" focusable={false}>
 | 
			
		||||
          <path d="M10.4,4h3.2c2.2,0,3,0.2,3.9,0.7c0.8,0.4,1.5,1.1,1.9,1.9s0.7,1.6,0.7,3.9v3.2c0,2.2-0.2,3-0.7,3.9c-0.4,0.8-1.1,1.5-1.9,1.9s-1.6,0.7-3.9,0.7h-3.2c-2.2,0-3-0.2-3.9-0.7c-0.8-0.4-1.5-1.1-1.9-1.9c-0.4-1-0.6-1.8-0.6-4v-3.2c0-2.2,0.2-3,0.7-3.9C5.1,5.7,5.8,5,6.6,4.6C7.4,4.2,8.2,4,10.4,4z" />
 | 
			
		||||
        </ProjectPillColor>
 | 
			
		||||
        <ProjectPillName>{children}</ProjectPillName>
 | 
			
		||||
      </ProjectPillContents>
 | 
			
		||||
    </ProjectPill>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const OptionWrapper = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: 40px;
 | 
			
		||||
  padding: 0 16px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: #414561;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const OptionLabel = styled.div`
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const OptionTitle = styled.div`
 | 
			
		||||
  min-width: 50px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
const OptionSubTitle = styled.div`
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  font-size: 11px;
 | 
			
		||||
  margin-left: 8px;
 | 
			
		||||
  min-width: 50px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
`;
 | 
			
		||||
const Option = ({ innerProps, data }: any) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <OptionWrapper {...innerProps}>
 | 
			
		||||
      <OptionLabel>
 | 
			
		||||
        <OptionTitle>{data.label}</OptionTitle>
 | 
			
		||||
        <OptionSubTitle>{data.label}</OptionSubTitle>
 | 
			
		||||
      </OptionLabel>
 | 
			
		||||
    </OptionWrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const TaskGroupHeaderContents = styled.div<{ width: number }>`
 | 
			
		||||
  width: ${p => p.width}px;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  margin-left: 24px;
 | 
			
		||||
  line-height: 20px;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex: 1 1 auto;
 | 
			
		||||
  min-height: 30px;
 | 
			
		||||
  padding-right: 32px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  border-bottom: 1px solid transparent;
 | 
			
		||||
  border-top: 1px solid transparent;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const TaskGroupMinify = styled.div`
 | 
			
		||||
  height: 28px;
 | 
			
		||||
  min-height: 28px;
 | 
			
		||||
  min-width: 28px;
 | 
			
		||||
  width: 28px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  transition-duration: 0.2s;
 | 
			
		||||
  transition-property: background, border, box-shadow, fill;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.primary};
 | 
			
		||||
    transition-duration: 0.2s;
 | 
			
		||||
    transition-property: background, border, box-shadow, fill;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
const TaskGroupName = styled.div`
 | 
			
		||||
  flex-grow: 1;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: 50px;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  color: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// HEADER
 | 
			
		||||
const ScrollContainer = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex: 1 1 auto;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  min-height: 1px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const Row = styled.div`
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const RowHeaderLeft = styled.div<{ width: number }>`
 | 
			
		||||
  width: ${p => p.width}px;
 | 
			
		||||
 | 
			
		||||
  align-items: stretch;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
`;
 | 
			
		||||
const RowHeaderLeftInner = styled.div`
 | 
			
		||||
  align-items: stretch;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex: 1 0 auto;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  margin-right: -1px;
 | 
			
		||||
  padding-left: 24px;
 | 
			
		||||
`;
 | 
			
		||||
const RowHeaderLeftName = styled.div`
 | 
			
		||||
  position: relative;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  border-right: 1px solid #414561;
 | 
			
		||||
  border-top: 1px solid #414561;
 | 
			
		||||
  border-bottom: 1px solid #414561;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex: 1 0 auto;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const RowHeaderLeftNameText = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const RowHeaderRight = styled.div<{ left: number }>`
 | 
			
		||||
  left: ${p => p.left}px;
 | 
			
		||||
  right: 0px;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const RowScrollable = styled.div`
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const RowScrollContent = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const RowHeaderRightContainer = styled.div`
 | 
			
		||||
  padding-right: 24px;
 | 
			
		||||
 | 
			
		||||
  align-items: stretch;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex: 1 0 auto;
 | 
			
		||||
  height: 37px;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  margin: -1px 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ItemWrapper = styled.div<{ width: number }>`
 | 
			
		||||
  width: ${p => p.width}px;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  border: 1px solid #414561;
 | 
			
		||||
  border-bottom: 0;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  margin-right: -1px;
 | 
			
		||||
  padding: 0 8px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  border-bottom: 1px solid #414561;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: ${props => props.theme.colors.primary};
 | 
			
		||||
    color: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
const ItemsContainer = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  & ${ItemWrapper}:last-child {
 | 
			
		||||
    border-right: 0;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ItemName = styled.div`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
`;
 | 
			
		||||
type DateEditorState = {
 | 
			
		||||
  open: boolean;
 | 
			
		||||
  pos: { top: number; left: number } | null;
 | 
			
		||||
  task: null | Task;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ProjectEditorState = {
 | 
			
		||||
  open: boolean;
 | 
			
		||||
  pos: { top: number; left: number } | null;
 | 
			
		||||
  task: null | Task;
 | 
			
		||||
};
 | 
			
		||||
const RIGHT_ROW_WIDTH = 327;
 | 
			
		||||
 | 
			
		||||
const Projects = () => {
 | 
			
		||||
  const leftRow = window.innerWidth - RIGHT_ROW_WIDTH;
 | 
			
		||||
  const [menuOpen, setMenuOpen] = useState(false);
 | 
			
		||||
  const [filters, setFilters] = useStickyState<{ sort: MyTasksSort; status: MyTasksStatus }>(
 | 
			
		||||
    { sort: MyTasksSort.None, status: MyTasksStatus.All },
 | 
			
		||||
    'my_tasks_filter',
 | 
			
		||||
  );
 | 
			
		||||
  const { data } = useMyTasksQuery({
 | 
			
		||||
    variables: { sort: filters.sort, status: filters.status },
 | 
			
		||||
    fetchPolicy: 'cache-and-network',
 | 
			
		||||
  });
 | 
			
		||||
  const [dateEditor, setDateEditor] = useState<DateEditorState>({ open: false, pos: null, task: null });
 | 
			
		||||
  const onEditDueDate = (task: Task, $target: React.RefObject<HTMLElement>) => {
 | 
			
		||||
    if ($target && $target.current && data) {
 | 
			
		||||
      const pos = $target.current.getBoundingClientRect();
 | 
			
		||||
      setDateEditor({
 | 
			
		||||
        open: true,
 | 
			
		||||
        pos: {
 | 
			
		||||
          top: pos.top,
 | 
			
		||||
          left: pos.right,
 | 
			
		||||
        },
 | 
			
		||||
        task,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  const [newTask, setNewTask] = useState<{ open: boolean }>({ open: false });
 | 
			
		||||
  const match = useRouteMatch();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const [projectEditor, setProjectEditor] = useState<ProjectEditorState>({ open: false, pos: null, task: null });
 | 
			
		||||
  const onEditProject = ($target: React.RefObject<HTMLElement>) => {
 | 
			
		||||
    if ($target && $target.current) {
 | 
			
		||||
      const pos = $target.current.getBoundingClientRect();
 | 
			
		||||
      setProjectEditor({
 | 
			
		||||
        open: true,
 | 
			
		||||
        pos: {
 | 
			
		||||
          top: pos.top,
 | 
			
		||||
          left: pos.right,
 | 
			
		||||
        },
 | 
			
		||||
        task: null,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const [updateTaskDueDate] = useUpdateTaskDueDateMutation();
 | 
			
		||||
  const $editorContents = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $dateContents = useRef<HTMLDivElement>(null);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (dateEditor.open && $dateContents.current && dateEditor.task) {
 | 
			
		||||
      showPopup(
 | 
			
		||||
        $dateContents,
 | 
			
		||||
        <Popup tab={0} title={null}>
 | 
			
		||||
          <DueDateManager
 | 
			
		||||
            task={dateEditor.task}
 | 
			
		||||
            onCancel={() => null}
 | 
			
		||||
            onDueDateChange={(task, dueDate, hasTime) => {
 | 
			
		||||
              if (dateEditor.task) {
 | 
			
		||||
                updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate, hasTime } });
 | 
			
		||||
                setDateEditor(prev => ({ ...prev, task: { ...task, dueDate: dueDate.toISOString(), hasTime } }));
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            onRemoveDueDate={task => {
 | 
			
		||||
              if (dateEditor.task) {
 | 
			
		||||
                updateTaskDueDate({ variables: { taskID: dateEditor.task.id, dueDate: null, hasTime: false } });
 | 
			
		||||
                setDateEditor(prev => ({ ...prev, task: { ...task, hasTime: false } }));
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </Popup>,
 | 
			
		||||
        { onClose: () => setDateEditor({ open: false, task: null, pos: null }) },
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }, [dateEditor]);
 | 
			
		||||
 | 
			
		||||
  const [createTask] = useCreateTaskMutation({
 | 
			
		||||
    update: (client, newTaskData) => {
 | 
			
		||||
      updateApolloCache<MyTasksQuery>(
 | 
			
		||||
        client,
 | 
			
		||||
        MyTasksDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newTaskData.data) {
 | 
			
		||||
              draftCache.myTasks.tasks.unshift(newTaskData.data.createTask);
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { status: MyTasksStatus.All, sort: MyTasksSort.None },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const [setTaskComplete] = useSetTaskCompleteMutation();
 | 
			
		||||
  const [updateTaskName] = useUpdateTaskNameMutation();
 | 
			
		||||
  const [minified, setMinified] = useStickyState<Array<string>>([], 'my_tasks_minified');
 | 
			
		||||
  useOnOutsideClick(
 | 
			
		||||
    $editorContents,
 | 
			
		||||
    projectEditor.open,
 | 
			
		||||
    () =>
 | 
			
		||||
      setProjectEditor({
 | 
			
		||||
        open: false,
 | 
			
		||||
        task: null,
 | 
			
		||||
        pos: null,
 | 
			
		||||
      }),
 | 
			
		||||
    null,
 | 
			
		||||
  );
 | 
			
		||||
  if (data) {
 | 
			
		||||
    const groups: Array<Group> = [];
 | 
			
		||||
    if (filters.sort === MyTasksSort.None) {
 | 
			
		||||
      groups.push({
 | 
			
		||||
        id: 'recently-assigned',
 | 
			
		||||
        name: 'Recently Assigned',
 | 
			
		||||
        tasks: data.myTasks.tasks.map(task => ({
 | 
			
		||||
          ...task,
 | 
			
		||||
          labels: [],
 | 
			
		||||
          position: 0,
 | 
			
		||||
        })),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      let { tasks } = data.myTasks;
 | 
			
		||||
      if (filters.sort === MyTasksSort.DueDate) {
 | 
			
		||||
        const group: Group = { id: 'due_date', name: null, tasks: [] };
 | 
			
		||||
        data.myTasks.tasks.forEach(task => {
 | 
			
		||||
          if (task.dueDate) {
 | 
			
		||||
            group.tasks.push({ ...task, labels: [], position: 0 });
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        groups.push(group);
 | 
			
		||||
        tasks = tasks.filter(t => t.dueDate === null);
 | 
			
		||||
      }
 | 
			
		||||
      const projects = new Map<string, Array<Task>>();
 | 
			
		||||
      data.myTasks.projects.forEach(p => {
 | 
			
		||||
        if (!projects.has(p.projectID)) {
 | 
			
		||||
          projects.set(p.projectID, []);
 | 
			
		||||
        }
 | 
			
		||||
        const prev = projects.get(p.projectID);
 | 
			
		||||
        const task = tasks.find(t => t.id === p.taskID);
 | 
			
		||||
        if (prev && task) {
 | 
			
		||||
          projects.set(p.projectID, [...prev, { ...task, labels: [], position: 0 }]);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      for (const [id, pTasks] of projects) {
 | 
			
		||||
        const project = data.projects.find(c => c.id === id);
 | 
			
		||||
        if (pTasks.length === 0) continue;
 | 
			
		||||
        if (project) {
 | 
			
		||||
          groups.push({
 | 
			
		||||
            id,
 | 
			
		||||
            name: project.name,
 | 
			
		||||
            tasks: pTasks.sort((a, b) => {
 | 
			
		||||
              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;
 | 
			
		||||
              const first = dayjs(a.dueDate);
 | 
			
		||||
              const second = dayjs(b.dueDate);
 | 
			
		||||
              if (first.isSame(second, 'minute')) return 0;
 | 
			
		||||
              if (first.isAfter(second)) return -1;
 | 
			
		||||
              return 1;
 | 
			
		||||
            }),
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      groups.sort((a, b) => {
 | 
			
		||||
        if (a.name === null && b.name === null) return 0;
 | 
			
		||||
        if (a.name === null) return -1;
 | 
			
		||||
        if (b.name === null) return 1;
 | 
			
		||||
        return a.name.localeCompare(b.name);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
 | 
			
		||||
        <ProjectBar>
 | 
			
		||||
          <ProjectActions />
 | 
			
		||||
          <ProjectActions>
 | 
			
		||||
            <ProjectAction
 | 
			
		||||
              onClick={$target => {
 | 
			
		||||
                showPopup(
 | 
			
		||||
                  $target,
 | 
			
		||||
                  <MyTasksStatusPopup
 | 
			
		||||
                    status={filters.status}
 | 
			
		||||
                    onChangeStatus={status => {
 | 
			
		||||
                      setFilters(prev => ({ ...prev, status }));
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                  />,
 | 
			
		||||
                  { width: 185 },
 | 
			
		||||
                );
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <CheckCircleOutline width={13} height={13} />
 | 
			
		||||
              <ProjectActionText>{prettyStatus(filters.status)}</ProjectActionText>
 | 
			
		||||
            </ProjectAction>
 | 
			
		||||
            <ProjectAction
 | 
			
		||||
              onClick={$target => {
 | 
			
		||||
                showPopup(
 | 
			
		||||
                  $target,
 | 
			
		||||
                  <MyTasksSortPopup
 | 
			
		||||
                    sort={filters.sort}
 | 
			
		||||
                    onChangeSort={sort => {
 | 
			
		||||
                      setFilters(prev => ({ ...prev, sort }));
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                  />,
 | 
			
		||||
                  { width: 185 },
 | 
			
		||||
                );
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Sort width={13} height={13} />
 | 
			
		||||
              <ProjectActionText>{prettySort(filters.sort)}</ProjectActionText>
 | 
			
		||||
            </ProjectAction>
 | 
			
		||||
            <ProjectAction disabled>
 | 
			
		||||
              <Cogs width={13} height={13} />
 | 
			
		||||
              <ProjectActionText>Customize</ProjectActionText>
 | 
			
		||||
            </ProjectAction>
 | 
			
		||||
          </ProjectActions>
 | 
			
		||||
        </ProjectBar>
 | 
			
		||||
        <ScrollContainer>
 | 
			
		||||
          <Row>
 | 
			
		||||
            <RowHeaderLeft width={leftRow}>
 | 
			
		||||
              <RowHeaderLeftInner>
 | 
			
		||||
                <RowHeaderLeftName>
 | 
			
		||||
                  <RowHeaderLeftNameText>Task name</RowHeaderLeftNameText>
 | 
			
		||||
                </RowHeaderLeftName>
 | 
			
		||||
              </RowHeaderLeftInner>
 | 
			
		||||
            </RowHeaderLeft>
 | 
			
		||||
            <RowHeaderRight left={leftRow}>
 | 
			
		||||
              <RowScrollable>
 | 
			
		||||
                <RowScrollContent>
 | 
			
		||||
                  <RowHeaderRightContainer>
 | 
			
		||||
                    <ItemsContainer>
 | 
			
		||||
                      <ItemWrapper width={120}>
 | 
			
		||||
                        <ItemName>Due date</ItemName>
 | 
			
		||||
                      </ItemWrapper>
 | 
			
		||||
                      <ItemWrapper width={120}>
 | 
			
		||||
                        <ItemName>Project</ItemName>
 | 
			
		||||
                      </ItemWrapper>
 | 
			
		||||
                      <ItemWrapper width={50} />
 | 
			
		||||
                    </ItemsContainer>
 | 
			
		||||
                  </RowHeaderRightContainer>
 | 
			
		||||
                </RowScrollContent>
 | 
			
		||||
              </RowScrollable>
 | 
			
		||||
            </RowHeaderRight>
 | 
			
		||||
          </Row>
 | 
			
		||||
          <VerticalScoller>
 | 
			
		||||
            <VerticalScollerInner>
 | 
			
		||||
              <TableContents>
 | 
			
		||||
                {groups.map(group => {
 | 
			
		||||
                  const isMinified = minified.find(m => m === group.id) ?? false;
 | 
			
		||||
                  return (
 | 
			
		||||
                    <TaskGroupContainer key={group.id}>
 | 
			
		||||
                      {group.name && (
 | 
			
		||||
                        <TaskGroupHeader>
 | 
			
		||||
                          <TaskGroupHeaderContents width={leftRow}>
 | 
			
		||||
                            <TaskGroupMinify
 | 
			
		||||
                              onClick={() => {
 | 
			
		||||
                                setMinified(prev => {
 | 
			
		||||
                                  if (isMinified) {
 | 
			
		||||
                                    return prev.filter(c => c !== group.id);
 | 
			
		||||
                                  }
 | 
			
		||||
                                  return [...prev, group.id];
 | 
			
		||||
                                });
 | 
			
		||||
                              }}
 | 
			
		||||
                            >
 | 
			
		||||
                              {isMinified ? (
 | 
			
		||||
                                <CaretRight width={16} height={16} />
 | 
			
		||||
                              ) : (
 | 
			
		||||
                                <CaretDown width={16} height={16} />
 | 
			
		||||
                              )}
 | 
			
		||||
                            </TaskGroupMinify>
 | 
			
		||||
                            <TaskGroupName>{group.name}</TaskGroupName>
 | 
			
		||||
                          </TaskGroupHeaderContents>
 | 
			
		||||
                        </TaskGroupHeader>
 | 
			
		||||
                      )}
 | 
			
		||||
                      <TaskGroupItems>
 | 
			
		||||
                        {!isMinified &&
 | 
			
		||||
                          group.tasks.map(task => {
 | 
			
		||||
                            const projectID = data.myTasks.projects.find(t => t.taskID === task.id)?.projectID;
 | 
			
		||||
                            const projectName = data.projects.find(p => p.id === projectID)?.name;
 | 
			
		||||
                            return (
 | 
			
		||||
                              <TaskEntry
 | 
			
		||||
                                key={task.id}
 | 
			
		||||
                                complete={task.complete ?? false}
 | 
			
		||||
                                onToggleComplete={complete => {
 | 
			
		||||
                                  setTaskComplete({ variables: { taskID: task.id, complete } });
 | 
			
		||||
                                }}
 | 
			
		||||
                                onTaskDetails={() => {
 | 
			
		||||
                                  history.push(`${match.url}/c/${task.id}`);
 | 
			
		||||
                                }}
 | 
			
		||||
                                onRemoveDueDate={() => {
 | 
			
		||||
                                  updateTaskDueDate({ variables: { taskID: task.id, dueDate: null, hasTime: false } });
 | 
			
		||||
                                }}
 | 
			
		||||
                                project={projectName ?? 'none'}
 | 
			
		||||
                                dueDate={task.dueDate}
 | 
			
		||||
                                hasTime={task.hasTime ?? false}
 | 
			
		||||
                                name={task.name}
 | 
			
		||||
                                onEditName={name => updateTaskName({ variables: { taskID: task.id, name } })}
 | 
			
		||||
                                onEditProject={onEditProject}
 | 
			
		||||
                                onEditDueDate={$target => onEditDueDate({ ...task, position: 0, labels: [] }, $target)}
 | 
			
		||||
                              />
 | 
			
		||||
                            );
 | 
			
		||||
                          })}
 | 
			
		||||
                      </TaskGroupItems>
 | 
			
		||||
                    </TaskGroupContainer>
 | 
			
		||||
                  );
 | 
			
		||||
                })}
 | 
			
		||||
              </TableContents>
 | 
			
		||||
            </VerticalScollerInner>
 | 
			
		||||
          </VerticalScoller>
 | 
			
		||||
        </ScrollContainer>
 | 
			
		||||
        {dateEditor.open && dateEditor.pos !== null && dateEditor.task && (
 | 
			
		||||
          <EditorPositioner left={dateEditor.pos.left} top={dateEditor.pos.top}>
 | 
			
		||||
            <EditorPositionerContents ref={$dateContents}>
 | 
			
		||||
              <EditorContainer width={120}>
 | 
			
		||||
                <EditorCell width={120}>
 | 
			
		||||
                  <DueDateEditorLabel>
 | 
			
		||||
                    {dateEditor.task.dueDate
 | 
			
		||||
                      ? dayjs(dateEditor.task.dueDate).format(dateEditor.task.hasTime ? 'MMM D [at] h:mm A' : 'MMM D')
 | 
			
		||||
                      : ''}
 | 
			
		||||
                  </DueDateEditorLabel>
 | 
			
		||||
                </EditorCell>
 | 
			
		||||
              </EditorContainer>
 | 
			
		||||
            </EditorPositionerContents>
 | 
			
		||||
          </EditorPositioner>
 | 
			
		||||
        )}
 | 
			
		||||
        {projectEditor.open && projectEditor.pos !== null && (
 | 
			
		||||
          <EditorPositioner left={projectEditor.pos.left} top={projectEditor.pos.top}>
 | 
			
		||||
            <EditorPositionerContents ref={$editorContents}>
 | 
			
		||||
              <EditorContainer width={300}>
 | 
			
		||||
                <EditorCell width={300}>
 | 
			
		||||
                  <Select
 | 
			
		||||
                    components={{ SingleValue, Option }}
 | 
			
		||||
                    autoFocus
 | 
			
		||||
                    styles={editorColourStyles}
 | 
			
		||||
                    options={[{ label: 'hello', value: '1' }]}
 | 
			
		||||
                    onInputChange={(query, { action }) => {
 | 
			
		||||
                      if (action === 'input-change') {
 | 
			
		||||
                        setMenuOpen(true);
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                    onChange={() => setMenuOpen(false)}
 | 
			
		||||
                    onBlur={() => setMenuOpen(false)}
 | 
			
		||||
                    menuIsOpen={menuOpen}
 | 
			
		||||
                  />
 | 
			
		||||
                </EditorCell>
 | 
			
		||||
              </EditorContainer>
 | 
			
		||||
            </EditorPositionerContents>
 | 
			
		||||
          </EditorPositioner>
 | 
			
		||||
        )}
 | 
			
		||||
        <Route
 | 
			
		||||
          path={`${match.path}/c/:taskID`}
 | 
			
		||||
          render={(routeProps: RouteComponentProps<TaskRouteProps>) => (
 | 
			
		||||
            <Details
 | 
			
		||||
              refreshCache={NOOP}
 | 
			
		||||
              availableMembers={[]}
 | 
			
		||||
              projectURL={`${match.url}`}
 | 
			
		||||
              taskID={routeProps.match.params.taskID}
 | 
			
		||||
              onTaskNameChange={(updatedTask, newName) => {
 | 
			
		||||
                updateTaskName({ variables: { taskID: updatedTask.id, name: newName } });
 | 
			
		||||
              }}
 | 
			
		||||
              onTaskDescriptionChange={(updatedTask, newDescription) => {
 | 
			
		||||
                /*
 | 
			
		||||
                updateTaskDescription({
 | 
			
		||||
                  variables: { taskID: updatedTask.id, description: newDescription },
 | 
			
		||||
                  optimisticResponse: {
 | 
			
		||||
                    __typename: 'Mutation',
 | 
			
		||||
                    updateTaskDescription: {
 | 
			
		||||
                      __typename: 'Task',
 | 
			
		||||
                      id: updatedTask.id,
 | 
			
		||||
                      description: newDescription,
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                });
 | 
			
		||||
                */
 | 
			
		||||
              }}
 | 
			
		||||
              onDeleteTask={deletedTask => {
 | 
			
		||||
                // deleteTask({ variables: { taskID: deletedTask.id } });
 | 
			
		||||
                history.push(`${match.url}`);
 | 
			
		||||
              }}
 | 
			
		||||
              onOpenAddLabelPopup={(task, $targetRef) => {
 | 
			
		||||
                /*
 | 
			
		||||
                taskLabelsRef.current = task.labels;
 | 
			
		||||
                showPopup(
 | 
			
		||||
                  $targetRef,
 | 
			
		||||
                  <LabelManagerEditor
 | 
			
		||||
                    onLabelToggle={labelID => {
 | 
			
		||||
                      toggleTaskLabel({ variables: { taskID: task.id, projectLabelID: labelID } });
 | 
			
		||||
                    }}
 | 
			
		||||
                    labelColors={data.labelColors}
 | 
			
		||||
                    labels={labelsRef}
 | 
			
		||||
                    taskLabels={taskLabelsRef}
 | 
			
		||||
                    projectID={projectID}
 | 
			
		||||
                  />,
 | 
			
		||||
                );
 | 
			
		||||
                */
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        />
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Projects;
 | 
			
		||||
@@ -159,7 +159,6 @@ const FilterMeta: React.FC<FilterMetaProps> = ({ filters, onChangeTaskMetaFilter
 | 
			
		||||
            width="100%"
 | 
			
		||||
            onChange={e => handleNameChange(e.currentTarget.value)}
 | 
			
		||||
            value={nameFilter}
 | 
			
		||||
            autoFocus
 | 
			
		||||
            variant="alternate"
 | 
			
		||||
            placeholder="Task name..."
 | 
			
		||||
          />
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ export const ActionExtraMenuItem = styled.li`
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: ${props => props.theme.colors.primary};
 | 
			
		||||
    background: rgb(${props => props.theme.colors.primary});
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
const ActionExtraMenuSeparator = styled.li`
 | 
			
		||||
 
 | 
			
		||||
@@ -280,7 +280,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            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,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -296,11 +296,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            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 (newTaskData.data) {
 | 
			
		||||
                draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
 | 
			
		||||
              }
 | 
			
		||||
              draftCache.findProject.taskGroups[idx].tasks.push({ ...newTaskData.data.createTask });
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -315,9 +313,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newTaskGroupData.data) {
 | 
			
		||||
              draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
 | 
			
		||||
            }
 | 
			
		||||
            draftCache.findProject.taskGroups.push({ ...newTaskGroupData.data.createTaskGroup, tasks: [] });
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -336,7 +332,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            const idx = cache.findProject.taskGroups.findIndex(
 | 
			
		||||
              t => t.id === resp.data?.deleteTaskGroupTasks.taskGroupID,
 | 
			
		||||
              t => t.id === resp.data.deleteTaskGroupTasks.taskGroupID,
 | 
			
		||||
            );
 | 
			
		||||
            if (idx !== -1) {
 | 
			
		||||
              draftCache.findProject.taskGroups[idx].tasks = [];
 | 
			
		||||
@@ -352,9 +348,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (resp.data) {
 | 
			
		||||
              draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
 | 
			
		||||
            }
 | 
			
		||||
            draftCache.findProject.taskGroups.push(resp.data.duplicateTaskGroup.taskGroup);
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -370,24 +364,19 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newTask.data) {
 | 
			
		||||
              const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
 | 
			
		||||
              if (previousTaskGroupID !== task.taskGroup.id) {
 | 
			
		||||
                const { taskGroups } = cache.findProject;
 | 
			
		||||
                const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
 | 
			
		||||
                const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
 | 
			
		||||
                if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
 | 
			
		||||
                  const previousTask = cache.findProject.taskGroups[oldTaskGroupIdx].tasks.find(t => t.id === task.id);
 | 
			
		||||
                  draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
 | 
			
		||||
                    (t: Task) => t.id !== task.id,
 | 
			
		||||
                  );
 | 
			
		||||
                  if (previousTask) {
 | 
			
		||||
                    draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
 | 
			
		||||
                      ...taskGroups[newTaskGroupIdx].tasks,
 | 
			
		||||
                      { ...previousTask },
 | 
			
		||||
                    ];
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
            const { previousTaskGroupID, task } = newTask.data.updateTaskLocation;
 | 
			
		||||
            if (previousTaskGroupID !== task.taskGroup.id) {
 | 
			
		||||
              const { taskGroups } = cache.findProject;
 | 
			
		||||
              const oldTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === previousTaskGroupID);
 | 
			
		||||
              const newTaskGroupIdx = taskGroups.findIndex((t: TaskGroup) => t.id === task.taskGroup.id);
 | 
			
		||||
              if (oldTaskGroupIdx !== -1 && newTaskGroupIdx !== -1) {
 | 
			
		||||
                draftCache.findProject.taskGroups[oldTaskGroupIdx].tasks = taskGroups[oldTaskGroupIdx].tasks.filter(
 | 
			
		||||
                  (t: Task) => t.id !== task.id,
 | 
			
		||||
                );
 | 
			
		||||
                draftCache.findProject.taskGroups[newTaskGroupIdx].tasks = [
 | 
			
		||||
                  ...taskGroups[newTaskGroupIdx].tasks,
 | 
			
		||||
                  { ...task },
 | 
			
		||||
                ];
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
@@ -426,7 +415,6 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
              name,
 | 
			
		||||
              complete: false,
 | 
			
		||||
              completedAt: null,
 | 
			
		||||
              hasTime: false,
 | 
			
		||||
              taskGroup: {
 | 
			
		||||
                __typename: 'TaskGroup',
 | 
			
		||||
                id: taskGroup.id,
 | 
			
		||||
@@ -460,6 +448,9 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <BoardLoading />;
 | 
			
		||||
  }
 | 
			
		||||
  const getTaskStatusFilterLabel = (filter: TaskStatusFilter) => {
 | 
			
		||||
    if (filter.status === TaskStatus.COMPLETE) {
 | 
			
		||||
      return 'Complete';
 | 
			
		||||
@@ -794,12 +785,12 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
                  <DueDateManager
 | 
			
		||||
                    task={task}
 | 
			
		||||
                    onRemoveDueDate={t => {
 | 
			
		||||
                      updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
 | 
			
		||||
                      // hidePopup();
 | 
			
		||||
                      updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } });
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                    onDueDateChange={(t, newDueDate, hasTime) => {
 | 
			
		||||
                      updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
 | 
			
		||||
                      // hidePopup();
 | 
			
		||||
                    onDueDateChange={(t, newDueDate) => {
 | 
			
		||||
                      updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                    onCancel={NOOP}
 | 
			
		||||
                  />
 | 
			
		||||
@@ -816,7 +807,7 @@ const ProjectBoard: React.FC<ProjectBoardProps> = ({ projectID, onCardLabelClick
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <BoardLoading />;
 | 
			
		||||
  return <span>Error</span>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ProjectBoard;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import Modal from 'shared/components/Modal';
 | 
			
		||||
import TaskDetails from 'shared/components/TaskDetails';
 | 
			
		||||
import TaskDetailsLoading from 'shared/components/TaskDetails/Loading';
 | 
			
		||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
 | 
			
		||||
import MemberManager from 'shared/components/MemberManager';
 | 
			
		||||
import { useRouteMatch, useHistory } from 'react-router';
 | 
			
		||||
@@ -22,9 +21,6 @@ import {
 | 
			
		||||
  useCreateTaskChecklistItemMutation,
 | 
			
		||||
  FindTaskDocument,
 | 
			
		||||
  FindTaskQuery,
 | 
			
		||||
  useCreateTaskCommentMutation,
 | 
			
		||||
  useDeleteTaskCommentMutation,
 | 
			
		||||
  useUpdateTaskCommentMutation,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
import { useCurrentUser } from 'App/context';
 | 
			
		||||
import MiniProfile from 'shared/components/MiniProfile';
 | 
			
		||||
@@ -37,73 +33,6 @@ import { useForm } from 'react-hook-form';
 | 
			
		||||
import updateApolloCache from 'shared/utils/cache';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
 | 
			
		||||
export const ActionsList = styled.ul`
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionItem = styled.li`
 | 
			
		||||
  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};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionTitle = styled.span`
 | 
			
		||||
  margin-left: 20px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const WarningLabel = styled.p`
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  margin: 8px 12px;
 | 
			
		||||
`;
 | 
			
		||||
const DeleteConfirm = styled(Button)`
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 8px 12px;
 | 
			
		||||
  margin-bottom: 6px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type TaskCommentActionsProps = {
 | 
			
		||||
  onDeleteComment: () => void;
 | 
			
		||||
  onEditComment: () => void;
 | 
			
		||||
};
 | 
			
		||||
const TaskCommentActions: React.FC<TaskCommentActionsProps> = ({ onDeleteComment, onEditComment }) => {
 | 
			
		||||
  const { setTab } = usePopup();
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Popup tab={0} title={null}>
 | 
			
		||||
        <ActionsList>
 | 
			
		||||
          <ActionItem>
 | 
			
		||||
            <ActionTitle>Pin to top</ActionTitle>
 | 
			
		||||
          </ActionItem>
 | 
			
		||||
          <ActionItem onClick={() => onEditComment()}>
 | 
			
		||||
            <ActionTitle>Edit comment</ActionTitle>
 | 
			
		||||
          </ActionItem>
 | 
			
		||||
          <ActionItem onClick={() => setTab(1)}>
 | 
			
		||||
            <ActionTitle>Delete comment</ActionTitle>
 | 
			
		||||
          </ActionItem>
 | 
			
		||||
        </ActionsList>
 | 
			
		||||
      </Popup>
 | 
			
		||||
      <Popup tab={1} title="Delete comment?">
 | 
			
		||||
        <WarningLabel>Deleting a comment can not be undone.</WarningLabel>
 | 
			
		||||
        <DeleteConfirm onClick={() => onDeleteComment()} color="danger">
 | 
			
		||||
          Delete comment
 | 
			
		||||
        </DeleteConfirm>
 | 
			
		||||
      </Popup>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const calculateChecklistBadge = (checklists: Array<TaskChecklist>) => {
 | 
			
		||||
  const total = checklists.reduce((prev: any, next: any) => {
 | 
			
		||||
    return (
 | 
			
		||||
@@ -201,40 +130,6 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
  const { user } = useCurrentUser();
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const [deleteTaskComment] = useDeleteTaskCommentMutation({
 | 
			
		||||
    update: (client, response) => {
 | 
			
		||||
      updateApolloCache<FindTaskQuery>(
 | 
			
		||||
        client,
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
              draftCache.findTask.comments = cache.findTask.comments.filter(
 | 
			
		||||
                c => c.id !== response.data?.deleteTaskComment.commentID,
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const [createTaskComment] = useCreateTaskCommentMutation({
 | 
			
		||||
    update: (client, response) => {
 | 
			
		||||
      updateApolloCache<FindTaskQuery>(
 | 
			
		||||
        client,
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
              draftCache.findTask.comments.push({
 | 
			
		||||
                ...response.data.createTaskComment.comment,
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const [updateTaskChecklistLocation] = useUpdateTaskChecklistLocationMutation();
 | 
			
		||||
  const [updateTaskChecklistItemLocation] = useUpdateTaskChecklistItemLocationMutation({
 | 
			
		||||
    update: (client, response) => {
 | 
			
		||||
@@ -243,23 +138,21 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
              const { prevChecklistID, taskChecklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
 | 
			
		||||
              if (taskChecklistID !== prevChecklistID) {
 | 
			
		||||
                const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
 | 
			
		||||
                const newIdx = cache.findTask.checklists.findIndex(c => c.id === taskChecklistID);
 | 
			
		||||
                if (oldIdx > -1 && newIdx > -1) {
 | 
			
		||||
                  const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
 | 
			
		||||
                  if (item) {
 | 
			
		||||
                    draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
 | 
			
		||||
                      i => i.id !== checklistItem.id,
 | 
			
		||||
                    );
 | 
			
		||||
                    draftCache.findTask.checklists[newIdx].items.push({
 | 
			
		||||
                      ...item,
 | 
			
		||||
                      position: checklistItem.position,
 | 
			
		||||
                      taskChecklistID,
 | 
			
		||||
                    });
 | 
			
		||||
                  }
 | 
			
		||||
            const { prevChecklistID, checklistID, checklistItem } = response.data.updateTaskChecklistItemLocation;
 | 
			
		||||
            if (checklistID !== prevChecklistID) {
 | 
			
		||||
              const oldIdx = cache.findTask.checklists.findIndex(c => c.id === prevChecklistID);
 | 
			
		||||
              const newIdx = cache.findTask.checklists.findIndex(c => c.id === checklistID);
 | 
			
		||||
              if (oldIdx > -1 && newIdx > -1) {
 | 
			
		||||
                const item = cache.findTask.checklists[oldIdx].items.find(i => i.id === checklistItem.id);
 | 
			
		||||
                if (item) {
 | 
			
		||||
                  draftCache.findTask.checklists[oldIdx].items = cache.findTask.checklists[oldIdx].items.filter(
 | 
			
		||||
                    i => i.id !== checklistItem.id,
 | 
			
		||||
                  );
 | 
			
		||||
                  draftCache.findTask.checklists[newIdx].items.push({
 | 
			
		||||
                    ...item,
 | 
			
		||||
                    position: checklistItem.position,
 | 
			
		||||
                    taskChecklistID: checklistID,
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
@@ -295,7 +188,7 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            const { checklists } = cache.findTask;
 | 
			
		||||
            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);
 | 
			
		||||
            draftCache.findTask.badges.checklist = {
 | 
			
		||||
@@ -319,10 +212,8 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (createData.data) {
 | 
			
		||||
              const item = createData.data.createTaskChecklist;
 | 
			
		||||
              draftCache.findTask.checklists.push({ ...item });
 | 
			
		||||
            }
 | 
			
		||||
            const item = createData.data.createTaskChecklist;
 | 
			
		||||
            draftCache.findTask.checklists.push({ ...item });
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -336,21 +227,19 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (deleteData.data) {
 | 
			
		||||
              const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
 | 
			
		||||
              const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
 | 
			
		||||
              if (targetIdx > -1) {
 | 
			
		||||
                draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
 | 
			
		||||
                  c => item.id !== c.id,
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
              const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
 | 
			
		||||
              draftCache.findTask.badges.checklist = {
 | 
			
		||||
                __typename: 'ChecklistBadge',
 | 
			
		||||
                complete,
 | 
			
		||||
                total,
 | 
			
		||||
              };
 | 
			
		||||
            const item = deleteData.data.deleteTaskChecklistItem.taskChecklistItem;
 | 
			
		||||
            const targetIdx = cache.findTask.checklists.findIndex(c => c.id === item.taskChecklistID);
 | 
			
		||||
            if (targetIdx > -1) {
 | 
			
		||||
              draftCache.findTask.checklists[targetIdx].items = cache.findTask.checklists[targetIdx].items.filter(
 | 
			
		||||
                c => item.id !== c.id,
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
            const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
 | 
			
		||||
            draftCache.findTask.badges.checklist = {
 | 
			
		||||
              __typename: 'ChecklistBadge',
 | 
			
		||||
              complete,
 | 
			
		||||
              total,
 | 
			
		||||
            };
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -363,30 +252,24 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
        FindTaskDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newTaskItem.data) {
 | 
			
		||||
              const item = newTaskItem.data.createTaskChecklistItem;
 | 
			
		||||
              const { checklists } = cache.findTask;
 | 
			
		||||
              const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
 | 
			
		||||
              if (idx !== -1) {
 | 
			
		||||
                draftCache.findTask.checklists[idx].items.push({ ...item });
 | 
			
		||||
                const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
 | 
			
		||||
                draftCache.findTask.badges.checklist = {
 | 
			
		||||
                  __typename: 'ChecklistBadge',
 | 
			
		||||
                  complete,
 | 
			
		||||
                  total,
 | 
			
		||||
                };
 | 
			
		||||
              }
 | 
			
		||||
            const item = newTaskItem.data.createTaskChecklistItem;
 | 
			
		||||
            const { checklists } = cache.findTask;
 | 
			
		||||
            const idx = checklists.findIndex(c => c.id === item.taskChecklistID);
 | 
			
		||||
            if (idx !== -1) {
 | 
			
		||||
              draftCache.findTask.checklists[idx].items.push({ ...item });
 | 
			
		||||
              const { complete, total } = calculateChecklistBadge(draftCache.findTask.checklists);
 | 
			
		||||
              draftCache.findTask.badges.checklist = {
 | 
			
		||||
                __typename: 'ChecklistBadge',
 | 
			
		||||
                complete,
 | 
			
		||||
                total,
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { taskID },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data, refetch } = useFindTaskQuery({
 | 
			
		||||
    variables: { taskID },
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
    fetchPolicy: 'cache-and-network',
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data, refetch } = useFindTaskQuery({ variables: { taskID } });
 | 
			
		||||
  const [setTaskComplete] = useSetTaskCompleteMutation();
 | 
			
		||||
  const [updateTaskDueDate] = useUpdateTaskDueDateMutation({
 | 
			
		||||
    onCompleted: () => {
 | 
			
		||||
@@ -406,9 +289,12 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
      refreshCache();
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const [updateTaskComment] = useUpdateTaskCommentMutation();
 | 
			
		||||
  const [editableComment, setEditableComment] = useState<null | string>(null);
 | 
			
		||||
  const isLoading = true;
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  if (!data) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Modal
 | 
			
		||||
@@ -417,33 +303,10 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
          history.push(projectURL);
 | 
			
		||||
        }}
 | 
			
		||||
        renderContent={() => {
 | 
			
		||||
          return data ? (
 | 
			
		||||
          return (
 | 
			
		||||
            <TaskDetails
 | 
			
		||||
              onCancelCommentEdit={() => setEditableComment(null)}
 | 
			
		||||
              onUpdateComment={(commentID, message) => {
 | 
			
		||||
                updateTaskComment({ variables: { commentID, message } });
 | 
			
		||||
              }}
 | 
			
		||||
              editableComment={editableComment}
 | 
			
		||||
              me={data.me.user}
 | 
			
		||||
              onCommentShowActions={(commentID, $targetRef) => {
 | 
			
		||||
                showPopup(
 | 
			
		||||
                  $targetRef,
 | 
			
		||||
                  <TaskCommentActions
 | 
			
		||||
                    onDeleteComment={() => {
 | 
			
		||||
                      deleteTaskComment({ variables: { commentID } });
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                    onEditComment={() => {
 | 
			
		||||
                      setEditableComment(commentID);
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                  />,
 | 
			
		||||
                );
 | 
			
		||||
              }}
 | 
			
		||||
              task={data.findTask}
 | 
			
		||||
              onCreateComment={(task, message) => {
 | 
			
		||||
                createTaskComment({ variables: { taskID: task.id, message } });
 | 
			
		||||
              }}
 | 
			
		||||
              onChecklistDrop={checklist => {
 | 
			
		||||
                updateTaskChecklistLocation({
 | 
			
		||||
                  variables: { taskChecklistID: checklist.id, position: checklist.position },
 | 
			
		||||
@@ -632,12 +495,12 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
                    <DueDateManager
 | 
			
		||||
                      task={task}
 | 
			
		||||
                      onRemoveDueDate={t => {
 | 
			
		||||
                        updateTaskDueDate({ variables: { taskID: t.id, dueDate: null, hasTime: false } });
 | 
			
		||||
                        // hidePopup();
 | 
			
		||||
                        updateTaskDueDate({ variables: { taskID: t.id, dueDate: null } });
 | 
			
		||||
                        hidePopup();
 | 
			
		||||
                      }}
 | 
			
		||||
                      onDueDateChange={(t, newDueDate, hasTime) => {
 | 
			
		||||
                        updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate, hasTime } });
 | 
			
		||||
                        // hidePopup();
 | 
			
		||||
                      onDueDateChange={(t, newDueDate) => {
 | 
			
		||||
                        updateTaskDueDate({ variables: { taskID: t.id, dueDate: newDueDate } });
 | 
			
		||||
                        hidePopup();
 | 
			
		||||
                      }}
 | 
			
		||||
                      onCancel={NOOP}
 | 
			
		||||
                    />
 | 
			
		||||
@@ -646,8 +509,6 @@ const Details: React.FC<DetailsProps> = ({
 | 
			
		||||
                );
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <TaskDetailsLoading />
 | 
			
		||||
          );
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
 
 | 
			
		||||
@@ -36,9 +36,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (newLabelData.data) {
 | 
			
		||||
              draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
 | 
			
		||||
            }
 | 
			
		||||
            draftCache.findProject.labels.push({ ...newLabelData.data.createProjectLabel });
 | 
			
		||||
          }),
 | 
			
		||||
        {
 | 
			
		||||
          projectID,
 | 
			
		||||
@@ -55,7 +53,7 @@ const LabelManagerEditor: React.FC<LabelManagerEditorProps> = ({
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.labels = cache.findProject.labels.filter(
 | 
			
		||||
              label => label.id !== newLabelData.data?.deleteProjectLabel.id,
 | 
			
		||||
              label => label.id !== newLabelData.data.deleteProjectLabel.id,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,7 @@ import {
 | 
			
		||||
  FindProjectDocument,
 | 
			
		||||
  FindProjectQuery,
 | 
			
		||||
} from 'shared/generated/graphql';
 | 
			
		||||
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import UserContext, { useCurrentUser } from 'App/context';
 | 
			
		||||
import Input from 'shared/components/Input';
 | 
			
		||||
@@ -134,6 +135,7 @@ type MemberFilterOptions = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
 | 
			
		||||
  console.log(input.trim().length < 3);
 | 
			
		||||
  if (input && input.trim().length < 3) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
@@ -161,10 +163,12 @@ const fetchMembers = async (client: any, projectID: string, options: MemberFilte
 | 
			
		||||
 | 
			
		||||
  let results: any = [];
 | 
			
		||||
  const emails: Array<string> = [];
 | 
			
		||||
  console.log(res.data && res.data.searchMembers);
 | 
			
		||||
  if (res.data && res.data.searchMembers) {
 | 
			
		||||
    results = [
 | 
			
		||||
      ...res.data.searchMembers.map((m: any) => {
 | 
			
		||||
        if (m.status === 'INVITED') {
 | 
			
		||||
          console.log(`${m.id} is added`);
 | 
			
		||||
          return {
 | 
			
		||||
            label: m.id,
 | 
			
		||||
            value: {
 | 
			
		||||
@@ -176,15 +180,17 @@ const fetchMembers = async (client: any, projectID: string, options: MemberFilte
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          };
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log(`${m.user.email} is added`);
 | 
			
		||||
          emails.push(m.user.email);
 | 
			
		||||
          return {
 | 
			
		||||
            label: m.user.fullName,
 | 
			
		||||
            value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        emails.push(m.user.email);
 | 
			
		||||
        return {
 | 
			
		||||
          label: m.user.fullName,
 | 
			
		||||
          value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
 | 
			
		||||
        };
 | 
			
		||||
      }),
 | 
			
		||||
    ];
 | 
			
		||||
    console.log(results);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
 | 
			
		||||
@@ -237,6 +243,7 @@ const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
 | 
			
		||||
  console.log(data);
 | 
			
		||||
  return !isDisabled ? (
 | 
			
		||||
    <OptionWrapper {...innerProps} isFocused={isFocused}>
 | 
			
		||||
      <TaskAssignee
 | 
			
		||||
@@ -416,16 +423,14 @@ const Project = () => {
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (resp.data) {
 | 
			
		||||
              const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
 | 
			
		||||
                tg => tg.tasks.findIndex(t => t.id === resp.data?.deleteTask.taskID) !== -1,
 | 
			
		||||
              );
 | 
			
		||||
            const taskGroupIdx = draftCache.findProject.taskGroups.findIndex(
 | 
			
		||||
              tg => tg.tasks.findIndex(t => t.id === resp.data.deleteTask.taskID) !== -1,
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
              if (taskGroupIdx !== -1) {
 | 
			
		||||
                draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
 | 
			
		||||
                  taskGroupIdx
 | 
			
		||||
                ].tasks.filter(t => t.id !== resp.data?.deleteTask.taskID);
 | 
			
		||||
              }
 | 
			
		||||
            if (taskGroupIdx !== -1) {
 | 
			
		||||
              draftCache.findProject.taskGroups[taskGroupIdx].tasks = cache.findProject.taskGroups[
 | 
			
		||||
                taskGroupIdx
 | 
			
		||||
              ].tasks.filter(t => t.id !== resp.data.deleteTask.taskID);
 | 
			
		||||
            }
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -436,7 +441,6 @@ const Project = () => {
 | 
			
		||||
 | 
			
		||||
  const { loading, data, error } = useFindProjectQuery({
 | 
			
		||||
    variables: { projectID },
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const [updateProjectName] = useUpdateProjectNameMutation({
 | 
			
		||||
@@ -446,7 +450,7 @@ const Project = () => {
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.name = newName.data?.updateProjectName.name ?? '';
 | 
			
		||||
            draftCache.findProject.name = newName.data.updateProjectName.name;
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -460,16 +464,14 @@ const Project = () => {
 | 
			
		||||
        FindProjectDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
              draftCache.findProject.members = [
 | 
			
		||||
                ...cache.findProject.members,
 | 
			
		||||
                ...response.data.inviteProjectMembers.members,
 | 
			
		||||
              ];
 | 
			
		||||
              draftCache.findProject.invitedMembers = [
 | 
			
		||||
                ...cache.findProject.invitedMembers,
 | 
			
		||||
                ...response.data.inviteProjectMembers.invitedMembers,
 | 
			
		||||
              ];
 | 
			
		||||
            }
 | 
			
		||||
            draftCache.findProject.members = [
 | 
			
		||||
              ...cache.findProject.members,
 | 
			
		||||
              ...response.data.inviteProjectMembers.members,
 | 
			
		||||
            ];
 | 
			
		||||
            draftCache.findProject.invitedMembers = [
 | 
			
		||||
              ...cache.findProject.invitedMembers,
 | 
			
		||||
              ...response.data.inviteProjectMembers.invitedMembers,
 | 
			
		||||
            ];
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -483,7 +485,7 @@ const Project = () => {
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.invitedMembers = cache.findProject.invitedMembers.filter(
 | 
			
		||||
              m => m.email !== response.data?.deleteInvitedProjectMember.invitedMember.email ?? '',
 | 
			
		||||
              m => m.email !== response.data.deleteInvitedProjectMember.invitedMember.email,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -498,7 +500,7 @@ const Project = () => {
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findProject.members = cache.findProject.members.filter(
 | 
			
		||||
              m => m.id !== response.data?.deleteProjectMember.member.id,
 | 
			
		||||
              m => m.id !== response.data.deleteProjectMember.member.id,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { projectID },
 | 
			
		||||
@@ -517,6 +519,14 @@ const Project = () => {
 | 
			
		||||
      document.title = `${data.findProject.name} | Taskcafé`;
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <GlobalTopNavbar onSaveProjectName={NOOP} name="" projectID={null} />
 | 
			
		||||
        <BoardLoading />
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (error) {
 | 
			
		||||
    history.push('/projects');
 | 
			
		||||
  }
 | 
			
		||||
@@ -629,12 +639,7 @@ const Project = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <GlobalTopNavbar onSaveProjectName={NOOP} name="" projectID={null} />
 | 
			
		||||
      <BoardLoading />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <div>Error</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Project;
 | 
			
		||||
 
 | 
			
		||||
@@ -150,7 +150,6 @@ const Wrapper = styled.div`
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  align-items: flex-start;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectSectionTitleWrapper = styled.div`
 | 
			
		||||
@@ -203,7 +202,7 @@ type ShowNewProject = {
 | 
			
		||||
 | 
			
		||||
const Projects = () => {
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery({ pollInterval: 3000, fetchPolicy: 'cache-and-network' });
 | 
			
		||||
  const { loading, data } = useGetProjectsQuery({ fetchPolicy: 'network-only' });
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.title = 'Taskcafé';
 | 
			
		||||
  }, []);
 | 
			
		||||
@@ -211,9 +210,7 @@ const Projects = () => {
 | 
			
		||||
    update: (client, newProject) => {
 | 
			
		||||
      updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
 | 
			
		||||
        produce(cache, draftCache => {
 | 
			
		||||
          if (newProject.data) {
 | 
			
		||||
            draftCache.projects.push({ ...newProject.data.createProject });
 | 
			
		||||
          }
 | 
			
		||||
          draftCache.projects.push({ ...newProject.data.createProject });
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
@@ -225,13 +222,14 @@ const Projects = () => {
 | 
			
		||||
    update: (client, createData) => {
 | 
			
		||||
      updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
 | 
			
		||||
        produce(cache, draftCache => {
 | 
			
		||||
          if (createData.data) {
 | 
			
		||||
            draftCache.teams.push({ ...createData.data?.createTeam });
 | 
			
		||||
          }
 | 
			
		||||
          draftCache.teams.push({ ...createData.data.createTeam });
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const colors = theme.colors.multiColors;
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
@@ -393,7 +391,7 @@ const Projects = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />;
 | 
			
		||||
  return <div>Error!</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Projects;
 | 
			
		||||
 
 | 
			
		||||
@@ -419,11 +419,7 @@ type MembersProps = {
 | 
			
		||||
 | 
			
		||||
const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({
 | 
			
		||||
    variables: { teamID },
 | 
			
		||||
    fetchPolicy: 'cache-and-network',
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({ variables: { teamID } });
 | 
			
		||||
  const { user, setUserRoles } = useCurrentUser();
 | 
			
		||||
  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”.';
 | 
			
		||||
@@ -434,13 +430,11 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
        GetTeamDocument,
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            if (response.data) {
 | 
			
		||||
              draftCache.findTeam.members.push({
 | 
			
		||||
                ...response.data.createTeamMember.teamMember,
 | 
			
		||||
                member: { __typename: 'MemberList', projects: [], teams: [] },
 | 
			
		||||
                owned: { __typename: 'OwnedList', projects: [], teams: [] },
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
            draftCache.findTeam.members.push({
 | 
			
		||||
              ...response.data.createTeamMember.teamMember,
 | 
			
		||||
              member: { __typename: 'MemberList', projects: [], teams: [] },
 | 
			
		||||
              owned: { __typename: 'OwnedList', projects: [], teams: [] },
 | 
			
		||||
            });
 | 
			
		||||
          }),
 | 
			
		||||
        { teamID },
 | 
			
		||||
      );
 | 
			
		||||
@@ -465,13 +459,16 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
        cache =>
 | 
			
		||||
          produce(cache, draftCache => {
 | 
			
		||||
            draftCache.findTeam.members = cache.findTeam.members.filter(
 | 
			
		||||
              member => member.id !== response.data?.deleteTeamMember.userID,
 | 
			
		||||
              member => member.id !== response.data.deleteTeamMember.userID,
 | 
			
		||||
            );
 | 
			
		||||
          }),
 | 
			
		||||
        { teamID },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <span>loading</span>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
    return (
 | 
			
		||||
@@ -559,7 +556,7 @@ const Members: React.FC<MembersProps> = ({ teamID }) => {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <div>loading</div>;
 | 
			
		||||
  return <div>error</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Members;
 | 
			
		||||
 
 | 
			
		||||
@@ -155,11 +155,10 @@ type TeamProjectsProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({
 | 
			
		||||
    variables: { teamID },
 | 
			
		||||
    fetchPolicy: 'cache-and-network',
 | 
			
		||||
    pollInterval: 3000,
 | 
			
		||||
  });
 | 
			
		||||
  const { loading, data } = useGetTeamQuery({ variables: { teamID } });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return <span>loading</span>;
 | 
			
		||||
  }
 | 
			
		||||
  if (data) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ProjectsContainer>
 | 
			
		||||
@@ -190,7 +189,7 @@ const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
 | 
			
		||||
      </ProjectsContainer>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <span>loading</span>;
 | 
			
		||||
  return <span>error</span>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TeamProjects;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ const Wrapper = styled.div`
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
type TeamPopupProps = {
 | 
			
		||||
  history: History<any>;
 | 
			
		||||
  history: History<History.PoorMansUnknown>;
 | 
			
		||||
  name: string;
 | 
			
		||||
  teamID: string;
 | 
			
		||||
};
 | 
			
		||||
@@ -44,9 +44,9 @@ export const TeamPopup: React.FC<TeamPopupProps> = ({ history, name, teamID }) =
 | 
			
		||||
    update: (client, deleteData) => {
 | 
			
		||||
      updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
 | 
			
		||||
        produce(cache, draftCache => {
 | 
			
		||||
          draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data?.deleteTeam.team.id);
 | 
			
		||||
          draftCache.teams = cache.teams.filter((team: any) => team.id !== deleteData.data.deleteTeam.team.id);
 | 
			
		||||
          draftCache.projects = cache.projects.filter(
 | 
			
		||||
            (project: any) => project.team.id !== deleteData.data?.deleteTeam.team.id,
 | 
			
		||||
            (project: any) => project.team.id !== deleteData.data.deleteTeam.team.id,
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
@@ -94,6 +94,23 @@ const Teams = () => {
 | 
			
		||||
  const { user } = useCurrentUser();
 | 
			
		||||
  const [currentTab, setCurrentTab] = useState(0);
 | 
			
		||||
  const match = useRouteMatch();
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return (
 | 
			
		||||
      <GlobalTopNavbar
 | 
			
		||||
        menuType={[
 | 
			
		||||
          { name: 'Projects', link: `${match.url}` },
 | 
			
		||||
          { name: 'Members', link: `${match.url}/members` },
 | 
			
		||||
        ]}
 | 
			
		||||
        currentTab={currentTab}
 | 
			
		||||
        onSetTab={tab => {
 | 
			
		||||
          setCurrentTab(tab);
 | 
			
		||||
        }}
 | 
			
		||||
        onSaveProjectName={NOOP}
 | 
			
		||||
        projectID={null}
 | 
			
		||||
        name={null}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (data && user) {
 | 
			
		||||
    if (!user.isVisible(PermissionLevel.TEAM, PermissionObjectType.TEAM, teamID)) {
 | 
			
		||||
      return <Redirect to="/" />;
 | 
			
		||||
@@ -129,21 +146,7 @@ const Teams = () => {
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <GlobalTopNavbar
 | 
			
		||||
      menuType={[
 | 
			
		||||
        { name: 'Projects', link: `${match.url}` },
 | 
			
		||||
        { name: 'Members', link: `${match.url}/members` },
 | 
			
		||||
      ]}
 | 
			
		||||
      currentTab={currentTab}
 | 
			
		||||
      onSetTab={tab => {
 | 
			
		||||
        setCurrentTab(tab);
 | 
			
		||||
      }}
 | 
			
		||||
      onSaveProjectName={NOOP}
 | 
			
		||||
      projectID={null}
 | 
			
		||||
      name={null}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
  return <div>Error!</div>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Teams;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,13 +20,15 @@ import App from './App';
 | 
			
		||||
 | 
			
		||||
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
 | 
			
		||||
 | 
			
		||||
enableMapSet();
 | 
			
		||||
 | 
			
		||||
dayjs.extend(isSameOrAfter);
 | 
			
		||||
 | 
			
		||||
dayjs.extend(weekday);
 | 
			
		||||
dayjs.extend(isBetween);
 | 
			
		||||
dayjs.extend(customParseFormat);
 | 
			
		||||
enableMapSet();
 | 
			
		||||
 | 
			
		||||
dayjs.extend(updateLocale);
 | 
			
		||||
 | 
			
		||||
dayjs.updateLocale('en', {
 | 
			
		||||
  week: {
 | 
			
		||||
    dow: 1, // First day of week is Monday
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								frontend/src/shared/components/AddList/AddList.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/shared/components/AddList/AddList.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import AddList from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: AddList,
 | 
			
		||||
  title: 'AddList',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: theme.colors.bg.secondary, default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return <AddList onSave={action('on save')} />;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										61
									
								
								frontend/src/shared/components/Admin/Admin.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								frontend/src/shared/components/Admin/Admin.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { ThemeProvider } from 'styled-components';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import Admin from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Admin,
 | 
			
		||||
  title: 'Admin',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8', default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <ThemeProvider theme={theme}>
 | 
			
		||||
        <Admin
 | 
			
		||||
          onInviteUser={action('invite user')}
 | 
			
		||||
          canInviteUser
 | 
			
		||||
          initialTab={1}
 | 
			
		||||
          onUpdateUserPassword={action('update user password')}
 | 
			
		||||
          onDeleteUser={action('delete user')}
 | 
			
		||||
          users={[
 | 
			
		||||
            {
 | 
			
		||||
              id: '1',
 | 
			
		||||
              username: 'jordanthedev',
 | 
			
		||||
              email: 'jordan@jordanthedev.com',
 | 
			
		||||
              role: { code: 'admin', name: 'Admin' },
 | 
			
		||||
              fullName: 'Jordan Knott',
 | 
			
		||||
              profileIcon: {
 | 
			
		||||
                bgColor: '#fff',
 | 
			
		||||
                initials: 'JK',
 | 
			
		||||
                url: null,
 | 
			
		||||
              },
 | 
			
		||||
              owned: {
 | 
			
		||||
                teams: [{ id: '1', name: 'Team' }],
 | 
			
		||||
                projects: [{ id: '2', name: 'Project' }],
 | 
			
		||||
              },
 | 
			
		||||
              member: {
 | 
			
		||||
                teams: [],
 | 
			
		||||
                projects: [],
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          ]}
 | 
			
		||||
          invitedUsers={[]}
 | 
			
		||||
          onAddUser={action('add user')}
 | 
			
		||||
          onDeleteInvitedUser={action('delete invited user')}
 | 
			
		||||
        />
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										138
									
								
								frontend/src/shared/components/Button/Button.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								frontend/src/shared/components/Button/Button.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import styled, { ThemeProvider } from 'styled-components';
 | 
			
		||||
import Button from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Button,
 | 
			
		||||
  title: 'Button',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8', default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ButtonRow = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-items: center;
 | 
			
		||||
  margin: 25px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  & > button {
 | 
			
		||||
    margin-right: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <ThemeProvider theme={theme}>
 | 
			
		||||
        <ButtonRow>
 | 
			
		||||
          <Button>Primary</Button>
 | 
			
		||||
          <Button color="success">Success</Button>
 | 
			
		||||
          <Button color="danger">Danger</Button>
 | 
			
		||||
          <Button color="warning">Warning</Button>
 | 
			
		||||
          <Button color="dark">Dark</Button>
 | 
			
		||||
          <Button disabled>Disabled</Button>
 | 
			
		||||
        </ButtonRow>
 | 
			
		||||
        <ButtonRow>
 | 
			
		||||
          <Button variant="outline">Primary</Button>
 | 
			
		||||
          <Button variant="outline" color="success">
 | 
			
		||||
            Success
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="outline" color="danger">
 | 
			
		||||
            Danger
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="outline" color="warning">
 | 
			
		||||
            Warning
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="outline" color="dark">
 | 
			
		||||
            Dark
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="outline" disabled>
 | 
			
		||||
            Disabled
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonRow>
 | 
			
		||||
        <ButtonRow>
 | 
			
		||||
          <Button variant="flat">Primary</Button>
 | 
			
		||||
          <Button variant="flat" color="success">
 | 
			
		||||
            Success
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="flat" color="danger">
 | 
			
		||||
            Danger
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="flat" color="warning">
 | 
			
		||||
            Warning
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="flat" color="dark">
 | 
			
		||||
            Dark
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="flat" disabled>
 | 
			
		||||
            Disabled
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonRow>
 | 
			
		||||
        <ButtonRow>
 | 
			
		||||
          <Button variant="lineDown">Primary</Button>
 | 
			
		||||
          <Button variant="lineDown" color="success">
 | 
			
		||||
            Success
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="lineDown" color="danger">
 | 
			
		||||
            Danger
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="lineDown" color="warning">
 | 
			
		||||
            Warning
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="lineDown" color="dark">
 | 
			
		||||
            Dark
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="lineDown" disabled>
 | 
			
		||||
            Disabled
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonRow>
 | 
			
		||||
        <ButtonRow>
 | 
			
		||||
          <Button variant="gradient">Primary</Button>
 | 
			
		||||
          <Button variant="gradient" color="success">
 | 
			
		||||
            Success
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="gradient" color="danger">
 | 
			
		||||
            Danger
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="gradient" color="warning">
 | 
			
		||||
            Warning
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="gradient" color="dark">
 | 
			
		||||
            Dark
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="gradient" disabled>
 | 
			
		||||
            Disabled
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonRow>
 | 
			
		||||
        <ButtonRow>
 | 
			
		||||
          <Button variant="relief">Primary</Button>
 | 
			
		||||
          <Button variant="relief" color="success">
 | 
			
		||||
            Success
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="relief" color="danger">
 | 
			
		||||
            Danger
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="relief" color="warning">
 | 
			
		||||
            Warning
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="relief" color="dark">
 | 
			
		||||
            Dark
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="relief" disabled>
 | 
			
		||||
            Disabled
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ButtonRow>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										174
									
								
								frontend/src/shared/components/Card/Card.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								frontend/src/shared/components/Card/Card.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,174 @@
 | 
			
		||||
import React, { useRef } from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import LabelColors from 'shared/constants/labelColors';
 | 
			
		||||
import Card from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Card,
 | 
			
		||||
  title: 'Card',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8', default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const labelData: Array<ProjectLabel> = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 'development',
 | 
			
		||||
    name: 'Development',
 | 
			
		||||
    createdDate: new Date().toString(),
 | 
			
		||||
    labelColor: {
 | 
			
		||||
      id: '1',
 | 
			
		||||
      colorHex: LabelColors.BLUE,
 | 
			
		||||
      name: 'blue',
 | 
			
		||||
      position: 1,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  const $ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      description=""
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Labels = () => {
 | 
			
		||||
  const $ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      description=""
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      labels={labelData}
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Badges = () => {
 | 
			
		||||
  const $ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      description="hello!"
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
      watched
 | 
			
		||||
      checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
      dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const PastDue = () => {
 | 
			
		||||
  const $ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      description="hello!"
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
      watched
 | 
			
		||||
      checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
      dueDate={{ isPastDue: true, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Everything = () => {
 | 
			
		||||
  const $ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      description="hello!"
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
      watched
 | 
			
		||||
      members={[
 | 
			
		||||
        {
 | 
			
		||||
          id: '1',
 | 
			
		||||
          fullName: 'Jordan Knott',
 | 
			
		||||
          profileIcon: {
 | 
			
		||||
            bgColor: '#0079bf',
 | 
			
		||||
            initials: 'JK',
 | 
			
		||||
            url: null,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ]}
 | 
			
		||||
      labels={labelData}
 | 
			
		||||
      checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
      dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Members = () => {
 | 
			
		||||
  const $ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      description={null}
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
      members={[
 | 
			
		||||
        {
 | 
			
		||||
          id: '1',
 | 
			
		||||
          fullName: 'Jordan Knott',
 | 
			
		||||
          profileIcon: {
 | 
			
		||||
            bgColor: '#0079bf',
 | 
			
		||||
            initials: 'JK',
 | 
			
		||||
            url: null,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ]}
 | 
			
		||||
      labels={[]}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Editable = () => {
 | 
			
		||||
  const $ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      description="hello!"
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
      watched
 | 
			
		||||
      labels={labelData}
 | 
			
		||||
      checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
      dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
      editable
 | 
			
		||||
      onEditCard={action('edit card')}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -227,19 +227,18 @@ export const ListCardOperation = styled.span`
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CardTitle = styled.div`
 | 
			
		||||
export const CardTitle = styled.span`
 | 
			
		||||
  clear: both;
 | 
			
		||||
  margin: 0 0 4px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  display: block;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
`;
 | 
			
		||||
export const CardTitleText = styled.span`
 | 
			
		||||
  word-wrap: break-word;
 | 
			
		||||
  line-height: 18px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CardMembers = styled.div`
 | 
			
		||||
@@ -252,7 +251,6 @@ export const CompleteIcon = styled(CheckCircle)`
 | 
			
		||||
  fill: ${props => props.theme.colors.success};
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  margin-bottom: -2px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const EditorContent = styled.div`
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,6 @@ import {
 | 
			
		||||
  ListCardOperation,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
  CardMembers,
 | 
			
		||||
  CardTitleText,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
 | 
			
		||||
type DueDate = {
 | 
			
		||||
@@ -211,7 +210,7 @@ const Card = React.forwardRef(
 | 
			
		||||
            ) : (
 | 
			
		||||
              <CardTitle>
 | 
			
		||||
                {complete && <CompleteIcon width={16} height={16} />}
 | 
			
		||||
                <CardTitleText>{`${title}${position ? ` - ${position}` : ''}`}</CardTitleText>
 | 
			
		||||
                {`${title}${position ? ` - ${position}` : ''}`}
 | 
			
		||||
              </CardTitle>
 | 
			
		||||
            )}
 | 
			
		||||
            <ListCardBadges>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import CardComposer from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: CardComposer,
 | 
			
		||||
  title: 'CardComposer',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8', default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return <CardComposer isOpen onClose={action('on close')} onCreateCard={action('on create card')} />;
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useState, useRef, useEffect } from 'react';
 | 
			
		||||
import React, { useState, useRef } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
 | 
			
		||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
			
		||||
@@ -25,11 +25,6 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
 | 
			
		||||
  const $cardRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  useOnOutsideClick($cardRef, true, onClose, null);
 | 
			
		||||
  useOnEscapeKeyDown(isOpen, onClose);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if ($cardRef.current) {
 | 
			
		||||
      $cardRef.current.scrollIntoView();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  return (
 | 
			
		||||
    <CardComposerWrapper isOpen={isOpen} ref={$cardRef}>
 | 
			
		||||
      <Card
 | 
			
		||||
@@ -38,10 +33,8 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
 | 
			
		||||
        taskGroupID=""
 | 
			
		||||
        editable
 | 
			
		||||
        onEditCard={(_taskGroupID, _taskID, name) => {
 | 
			
		||||
          if (cardName.trim() !== '') {
 | 
			
		||||
            onCreateCard(name.trim());
 | 
			
		||||
            setCardName('');
 | 
			
		||||
          }
 | 
			
		||||
          onCreateCard(name);
 | 
			
		||||
          setCardName('');
 | 
			
		||||
        }}
 | 
			
		||||
        onCardTitleChange={name => {
 | 
			
		||||
          setCardName(name);
 | 
			
		||||
@@ -52,10 +45,8 @@ const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
 | 
			
		||||
          <AddCardButton
 | 
			
		||||
            variant="relief"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              if (cardName.trim() !== '') {
 | 
			
		||||
                onCreateCard(cardName.trim());
 | 
			
		||||
                setCardName('');
 | 
			
		||||
              }
 | 
			
		||||
              onCreateCard(cardName);
 | 
			
		||||
              setCardName('');
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            Add Card
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										155
									
								
								frontend/src/shared/components/Checklist/Checklist.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								frontend/src/shared/components/Checklist/Checklist.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import styled, { ThemeProvider } from 'styled-components';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import Checklist, { ChecklistItem } from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Checklist,
 | 
			
		||||
  title: 'Checklist',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8', default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Container = styled.div`
 | 
			
		||||
  width: 552px;
 | 
			
		||||
  margin: 25px;
 | 
			
		||||
  border: 1px solid ${props => props.theme.colors.bg.primary};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const defaultItems = [
 | 
			
		||||
  {
 | 
			
		||||
    id: '1',
 | 
			
		||||
    position: 1,
 | 
			
		||||
    taskChecklistID: '1',
 | 
			
		||||
    complete: false,
 | 
			
		||||
    name: 'Tasks',
 | 
			
		||||
    assigned: null,
 | 
			
		||||
    dueDate: null,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: '2',
 | 
			
		||||
    taskChecklistID: '1',
 | 
			
		||||
    position: 2,
 | 
			
		||||
    complete: false,
 | 
			
		||||
    name: 'Projects',
 | 
			
		||||
    assigned: null,
 | 
			
		||||
    dueDate: null,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: '3',
 | 
			
		||||
    position: 3,
 | 
			
		||||
    taskChecklistID: '1',
 | 
			
		||||
    complete: false,
 | 
			
		||||
    name: 'Teams',
 | 
			
		||||
    assigned: null,
 | 
			
		||||
    dueDate: null,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: '4',
 | 
			
		||||
    position: 4,
 | 
			
		||||
    complete: false,
 | 
			
		||||
    taskChecklistID: '1',
 | 
			
		||||
    name: 'Organizations',
 | 
			
		||||
    assigned: null,
 | 
			
		||||
    dueDate: null,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  const [checklistName, setChecklistName] = useState('Checklist');
 | 
			
		||||
  const [items, setItems] = useState(defaultItems);
 | 
			
		||||
  const onToggleItem = (itemID: string, complete: boolean) => {
 | 
			
		||||
    setItems(
 | 
			
		||||
      produce(items, draftState => {
 | 
			
		||||
        const idx = items.findIndex(item => item.id === itemID);
 | 
			
		||||
        if (idx !== -1) {
 | 
			
		||||
          draftState[idx] = {
 | 
			
		||||
            ...draftState[idx],
 | 
			
		||||
            complete,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <ThemeProvider theme={theme}>
 | 
			
		||||
        <Container>
 | 
			
		||||
          <Checklist
 | 
			
		||||
            wrapperProps={{}}
 | 
			
		||||
            handleProps={{}}
 | 
			
		||||
            name={checklistName}
 | 
			
		||||
            checklistID="checklist-one"
 | 
			
		||||
            items={items}
 | 
			
		||||
            onDeleteChecklist={action('delete checklist')}
 | 
			
		||||
            onChangeName={currentName => {
 | 
			
		||||
              setChecklistName(currentName);
 | 
			
		||||
            }}
 | 
			
		||||
            onAddItem={itemName => {
 | 
			
		||||
              let position = 1;
 | 
			
		||||
              const lastItem = items[-1];
 | 
			
		||||
              if (lastItem) {
 | 
			
		||||
                position = lastItem.position * 2 + 1;
 | 
			
		||||
              }
 | 
			
		||||
              setItems([
 | 
			
		||||
                ...items,
 | 
			
		||||
                {
 | 
			
		||||
                  id: `${Math.random()}`,
 | 
			
		||||
                  name: itemName,
 | 
			
		||||
                  complete: false,
 | 
			
		||||
                  assigned: null,
 | 
			
		||||
                  dueDate: null,
 | 
			
		||||
                  position,
 | 
			
		||||
                  taskChecklistID: '1',
 | 
			
		||||
                },
 | 
			
		||||
              ]);
 | 
			
		||||
            }}
 | 
			
		||||
            onDeleteItem={itemID => {
 | 
			
		||||
              setItems(items.filter(item => item.id !== itemID));
 | 
			
		||||
            }}
 | 
			
		||||
            onChangeItemName={(itemID, currentName) => {
 | 
			
		||||
              setItems(
 | 
			
		||||
                produce(items, draftState => {
 | 
			
		||||
                  const idx = items.findIndex(item => item.id === itemID);
 | 
			
		||||
                  if (idx !== -1) {
 | 
			
		||||
                    draftState[idx] = {
 | 
			
		||||
                      ...draftState[idx],
 | 
			
		||||
                      name: currentName,
 | 
			
		||||
                    };
 | 
			
		||||
                  }
 | 
			
		||||
                }),
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
            onToggleItem={onToggleItem}
 | 
			
		||||
          >
 | 
			
		||||
            {items.map(item => (
 | 
			
		||||
              <ChecklistItem
 | 
			
		||||
                key={item.id}
 | 
			
		||||
                wrapperProps={{}}
 | 
			
		||||
                handleProps={{}}
 | 
			
		||||
                checklistID="id"
 | 
			
		||||
                itemID={item.id}
 | 
			
		||||
                name={item.name}
 | 
			
		||||
                complete={item.complete}
 | 
			
		||||
                onDeleteItem={NOOP}
 | 
			
		||||
                onChangeName={NOOP}
 | 
			
		||||
                onToggleItem={NOOP}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </Checklist>
 | 
			
		||||
        </Container>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import styled, { ThemeProvider } from 'styled-components';
 | 
			
		||||
import { User } from 'shared/icons';
 | 
			
		||||
 | 
			
		||||
import Input from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Input,
 | 
			
		||||
  title: 'Input',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Wrapper = styled.div`
 | 
			
		||||
  background: ${props => props.theme.colors.bg.primary};
 | 
			
		||||
  padding: 45px;
 | 
			
		||||
  margin: 25px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <ThemeProvider theme={theme}>
 | 
			
		||||
        <Wrapper>
 | 
			
		||||
          <Input label="Label placeholder" />
 | 
			
		||||
          <Input width="100%" placeholder="Placeholder" />
 | 
			
		||||
          <Input icon={<User width={20} height={20} />} width="100%" placeholder="Placeholder" />
 | 
			
		||||
        </Wrapper>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,65 @@
 | 
			
		||||
import React, { createRef, useState } from 'react';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import DropdownMenu from '.';
 | 
			
		||||
import theme from '../../../App/ThemeStyles';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: DropdownMenu,
 | 
			
		||||
  title: 'DropdownMenu',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
      { name: 'darkBlue', value: theme.colors.bg.secondary, default: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Container = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
`;
 | 
			
		||||
const Button = styled.div`
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
  padding: 15px 20px;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  background: #000;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  const [menu, setMenu] = useState({
 | 
			
		||||
    top: 0,
 | 
			
		||||
    left: 0,
 | 
			
		||||
    isOpen: false,
 | 
			
		||||
  });
 | 
			
		||||
  const $buttonRef: any = createRef();
 | 
			
		||||
  const onClick = () => {
 | 
			
		||||
    setMenu({
 | 
			
		||||
      isOpen: !menu.isOpen,
 | 
			
		||||
      left: $buttonRef.current.getBoundingClientRect().right,
 | 
			
		||||
      top: $buttonRef.current.getBoundingClientRect().bottom,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Container>
 | 
			
		||||
        <Button onClick={onClick} ref={$buttonRef}>
 | 
			
		||||
          Click me
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Container>
 | 
			
		||||
      {menu.isOpen && (
 | 
			
		||||
        <DropdownMenu
 | 
			
		||||
          onAdminConsole={action('admin')}
 | 
			
		||||
          onCloseDropdown={() => {
 | 
			
		||||
            setMenu({ top: 0, left: 0, isOpen: false });
 | 
			
		||||
          }}
 | 
			
		||||
          onLogout={action('on logout')}
 | 
			
		||||
          left={menu.left}
 | 
			
		||||
          top={menu.top}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,74 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import styled, { ThemeProvider } from 'styled-components';
 | 
			
		||||
import { Popup } from '../PopupMenu';
 | 
			
		||||
import DueDateManager from '.';
 | 
			
		||||
 | 
			
		||||
const PopupWrapper = styled.div`
 | 
			
		||||
  width: 310px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: DueDateManager,
 | 
			
		||||
  title: 'DueDateManager',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8', default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <ThemeProvider theme={theme}>
 | 
			
		||||
        <PopupWrapper>
 | 
			
		||||
          <Popup title={null} tab={0}>
 | 
			
		||||
            <DueDateManager
 | 
			
		||||
              task={{
 | 
			
		||||
                id: '1',
 | 
			
		||||
                taskGroup: { name: 'General', id: '1', position: 1 },
 | 
			
		||||
                name: 'Hello, world',
 | 
			
		||||
                position: 1,
 | 
			
		||||
                labels: [
 | 
			
		||||
                  {
 | 
			
		||||
                    id: 'soft-skills',
 | 
			
		||||
                    assignedDate: new Date().toString(),
 | 
			
		||||
                    projectLabel: {
 | 
			
		||||
                      createdDate: new Date().toString(),
 | 
			
		||||
                      id: 'label-soft-skills',
 | 
			
		||||
                      name: 'Soft Skills',
 | 
			
		||||
                      labelColor: {
 | 
			
		||||
                        id: '1',
 | 
			
		||||
                        name: 'white',
 | 
			
		||||
                        colorHex: '#fff',
 | 
			
		||||
                        position: 1,
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
                description: 'hello!',
 | 
			
		||||
                assigned: [
 | 
			
		||||
                  {
 | 
			
		||||
                    id: '1',
 | 
			
		||||
                    profileIcon: { url: null, initials: null, bgColor: null },
 | 
			
		||||
                    fullName: 'Jordan Knott',
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
              }}
 | 
			
		||||
              onCancel={action('cancel')}
 | 
			
		||||
              onDueDateChange={action('due date change')}
 | 
			
		||||
              onRemoveDueDate={action('remove due date')}
 | 
			
		||||
            />
 | 
			
		||||
          </Popup>
 | 
			
		||||
        </PopupWrapper>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -2,8 +2,6 @@ import styled from 'styled-components';
 | 
			
		||||
import Button from 'shared/components/Button';
 | 
			
		||||
import { mixin } from 'shared/utils/styles';
 | 
			
		||||
import Input from 'shared/components/Input';
 | 
			
		||||
import ControlledInput from 'shared/components/ControlledInput';
 | 
			
		||||
import { Clock } from 'shared/icons';
 | 
			
		||||
 | 
			
		||||
export const Wrapper = styled.div`
 | 
			
		||||
display: flex
 | 
			
		||||
@@ -19,11 +17,6 @@ display: flex
 | 
			
		||||
    z-index: 10000;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
  }
 | 
			
		||||
  & .react-datepicker__close-icon::after {
 | 
			
		||||
    background: none;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .react-datepicker-time__header {
 | 
			
		||||
    color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
@@ -98,24 +91,6 @@ display: flex
 | 
			
		||||
    border-bottom: 1px solid ${props => props.theme.colors.border};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & .react-datepicker__input-container input {
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.2);
 | 
			
		||||
  border-color: ${props => props.theme.colors.alternate};
 | 
			
		||||
  background: #262c49;
 | 
			
		||||
  box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.15);
 | 
			
		||||
padding: 0.7rem;
 | 
			
		||||
  color: #c2c6dc;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  transition: all 0.3s ease;
 | 
			
		||||
      font-size: 13px;
 | 
			
		||||
    line-height: 20px;
 | 
			
		||||
    padding: 0 12px;
 | 
			
		||||
  &:focus {
 | 
			
		||||
    box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
 | 
			
		||||
    border: 1px solid rgba(115, 103, 240);
 | 
			
		||||
    background: ${props => props.theme.colors.bg.primary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const DueDatePickerWrapper = styled.div`
 | 
			
		||||
@@ -135,44 +110,6 @@ export const RemoveDueDate = styled(Button)`
 | 
			
		||||
  margin: 0 0 0 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const AddDateRange = styled.div`
 | 
			
		||||
  opacity: 0.6;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  line-height: 16px;
 | 
			
		||||
  color: ${props => mixin.rgba(props.theme.colors.primary, 0.8)};
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: ${props => mixin.rgba(props.theme.colors.primary, 1)};
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const DateRangeInputs = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  margin-left: -4px;
 | 
			
		||||
  & > div:first-child,
 | 
			
		||||
  & > div:last-child {
 | 
			
		||||
    flex: 1 1 92px;
 | 
			
		||||
    margin-bottom: 4px;
 | 
			
		||||
    margin-left: 4px;
 | 
			
		||||
    min-width: 92px;
 | 
			
		||||
    width: initial;
 | 
			
		||||
  }
 | 
			
		||||
  & > ${AddDateRange} {
 | 
			
		||||
    margin-left: 4px;
 | 
			
		||||
    padding-left: 4px;
 | 
			
		||||
  }
 | 
			
		||||
  & > .react-datepicker-wrapper input {
 | 
			
		||||
    padding-bottom: 4px;
 | 
			
		||||
    padding-top: 4px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CancelDueDate = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
@@ -182,86 +119,15 @@ export const CancelDueDate = styled.div`
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const DueDateInput = styled(ControlledInput)`
 | 
			
		||||
export const DueDateInput = styled(Input)`
 | 
			
		||||
  margin-top: 15px;
 | 
			
		||||
  margin-bottom: 5px;
 | 
			
		||||
  padding-right: 10px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionsSeparator = styled.div`
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
  height: 1px;
 | 
			
		||||
export const ActionWrapper = styled.div`
 | 
			
		||||
  padding-top: 8px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  background: #414561;
 | 
			
		||||
  display: flex;
 | 
			
		||||
`;
 | 
			
		||||
export const ActionsWrapper = styled.div`
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  & .react-datepicker-wrapper {
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    width: 82px;
 | 
			
		||||
  }
 | 
			
		||||
  & .react-datepicker__input-container input {
 | 
			
		||||
    padding-bottom: 4px;
 | 
			
		||||
    padding-top: 4px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionClock = styled(Clock)`
 | 
			
		||||
  align-self: center;
 | 
			
		||||
  fill: ${props => props.theme.colors.primary};
 | 
			
		||||
  margin: 0 8px;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionLabel = styled.div`
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  line-height: 14px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActionIcon = styled.div`
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  min-height: 36px;
 | 
			
		||||
  min-width: 36px;
 | 
			
		||||
  width: 36px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
  svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.primary};
 | 
			
		||||
    transition-duration: 0.2s;
 | 
			
		||||
    transition-property: background, border, box-shadow, fill;
 | 
			
		||||
  }
 | 
			
		||||
  &:hover svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ClearButton = styled.div`
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  height: 36px;
 | 
			
		||||
  line-height: 36px;
 | 
			
		||||
  padding: 0 12px;
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  transition-duration: 0.2s;
 | 
			
		||||
  transition-property: background, border, box-shadow, color, fill;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  &:hover {
 | 
			
		||||
    color: ${props => props.theme.colors.text.secondary};
 | 
			
		||||
  }
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
`;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useState, useEffect, forwardRef, useRef, useCallback } from 'react';
 | 
			
		||||
import React, { useState, useEffect, forwardRef } from 'react';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import DatePicker from 'react-datepicker';
 | 
			
		||||
@@ -8,27 +8,11 @@ import { getYear, getMonth } from 'date-fns';
 | 
			
		||||
import { useForm, Controller } from 'react-hook-form';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Wrapper,
 | 
			
		||||
  RemoveDueDate,
 | 
			
		||||
  DueDateInput,
 | 
			
		||||
  DueDatePickerWrapper,
 | 
			
		||||
  ConfirmAddDueDate,
 | 
			
		||||
  DateRangeInputs,
 | 
			
		||||
  AddDateRange,
 | 
			
		||||
  ActionIcon,
 | 
			
		||||
  ActionsWrapper,
 | 
			
		||||
  ClearButton,
 | 
			
		||||
  ActionsSeparator,
 | 
			
		||||
  ActionClock,
 | 
			
		||||
  ActionLabel,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
import { Clock, Cross } from 'shared/icons';
 | 
			
		||||
import Select from 'react-select/src/Select';
 | 
			
		||||
import { Wrapper, ActionWrapper, RemoveDueDate, DueDateInput, DueDatePickerWrapper, ConfirmAddDueDate } from './Styles';
 | 
			
		||||
 | 
			
		||||
type DueDateManagerProps = {
 | 
			
		||||
  task: Task;
 | 
			
		||||
  onDueDateChange: (task: Task, newDueDate: Date, hasTime: boolean) => void;
 | 
			
		||||
  onDueDateChange: (task: Task, newDueDate: Date) => void;
 | 
			
		||||
  onRemoveDueDate: (task: Task) => void;
 | 
			
		||||
  onCancel: () => void;
 | 
			
		||||
};
 | 
			
		||||
@@ -68,20 +52,14 @@ const HeaderSelect = styled.select`
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding: 4px 6px;
 | 
			
		||||
  background: none;
 | 
			
		||||
  outline: none;
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  appearance: none;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
 | 
			
		||||
  & option {
 | 
			
		||||
    color: #c2c6dc;
 | 
			
		||||
    background: ${props => props.theme.colors.bg.primary};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  & option:hover {
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: ${props => props.theme.colors.bg.secondary};
 | 
			
		||||
    border: 1px solid ${props => props.theme.colors.primary};
 | 
			
		||||
    outline: none !important;
 | 
			
		||||
@@ -132,34 +110,15 @@ const HeaderActions = styled.div`
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onRemoveDueDate, onCancel }) => {
 | 
			
		||||
  const currentDueDate = task.dueDate ? dayjs(task.dueDate).toDate() : null;
 | 
			
		||||
  const now = dayjs();
 | 
			
		||||
  const { register, handleSubmit, errors, setValue, setError, formState, control } = useForm<DueDateFormData>();
 | 
			
		||||
 | 
			
		||||
  const [startDate, setStartDate] = useState<Date | null>(currentDueDate);
 | 
			
		||||
  const [endDate, setEndDate] = useState<Date | null>(currentDueDate);
 | 
			
		||||
  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),
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
  const [startDate, setStartDate] = useState(new Date());
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    debouncedChange(startDate, hasTime);
 | 
			
		||||
  }, [startDate, hasTime]);
 | 
			
		||||
    const newDate = dayjs(startDate).format('YYYY-MM-DD');
 | 
			
		||||
    setValue('endDate', newDate);
 | 
			
		||||
  }, [startDate]);
 | 
			
		||||
 | 
			
		||||
  const years = _.range(2010, getYear(new Date()) + 10, 1);
 | 
			
		||||
  const months = [
 | 
			
		||||
    'January',
 | 
			
		||||
@@ -175,21 +134,19 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
 | 
			
		||||
    'November',
 | 
			
		||||
    'December',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const onChange = (dates: any) => {
 | 
			
		||||
    const [start, end] = dates;
 | 
			
		||||
    setStartDate(start);
 | 
			
		||||
    setEndDate(end);
 | 
			
		||||
  const saveDueDate = (data: any) => {
 | 
			
		||||
    const newDate = dayjs(`${data.endDate} ${dayjs(data.endTime).format('h:mm A')}`, 'YYYY-MM-DD h:mm A');
 | 
			
		||||
    if (newDate.isValid()) {
 | 
			
		||||
      onDueDateChange(task, newDate.toDate());
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  const [isRange, setIsRange] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const CustomTimeInput = forwardRef(({ value, onClick, onChange, onBlur, onFocus }: any, $ref: any) => {
 | 
			
		||||
  const CustomTimeInput = forwardRef(({ value, onClick }: any, $ref: any) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <DueDateInput
 | 
			
		||||
        id="endTime"
 | 
			
		||||
        value={value}
 | 
			
		||||
        name="endTime"
 | 
			
		||||
        onChange={onChange}
 | 
			
		||||
        ref={$ref}
 | 
			
		||||
        width="100%"
 | 
			
		||||
        variant="alternate"
 | 
			
		||||
        label="Time"
 | 
			
		||||
@@ -197,119 +154,114 @@ const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange,
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Wrapper>
 | 
			
		||||
      <DateRangeInputs>
 | 
			
		||||
        <DatePicker
 | 
			
		||||
          selected={startDate}
 | 
			
		||||
          onChange={date => setStartDate(date)}
 | 
			
		||||
          popperClassName="picker-hidden"
 | 
			
		||||
          dateFormat="yyyy-MM-dd"
 | 
			
		||||
          disabledKeyboardNavigation
 | 
			
		||||
          isClearable
 | 
			
		||||
          placeholderText="Select due date"
 | 
			
		||||
        />
 | 
			
		||||
        {isRange ? (
 | 
			
		||||
          <DatePicker
 | 
			
		||||
            selected={startDate}
 | 
			
		||||
            isClearable
 | 
			
		||||
            onChange={date => setStartDate(date)}
 | 
			
		||||
            popperClassName="picker-hidden"
 | 
			
		||||
            dateFormat="yyyy-MM-dd"
 | 
			
		||||
            placeholderText="Select from date"
 | 
			
		||||
      <Form onSubmit={handleSubmit(saveDueDate)}>
 | 
			
		||||
        <FormField>
 | 
			
		||||
          <DueDateInput
 | 
			
		||||
            id="endDate"
 | 
			
		||||
            name="endDate"
 | 
			
		||||
            width="100%"
 | 
			
		||||
            variant="alternate"
 | 
			
		||||
            label="Date"
 | 
			
		||||
            defaultValue={now.format('YYYY-MM-DD')}
 | 
			
		||||
            ref={register({
 | 
			
		||||
              required: 'End date is required.',
 | 
			
		||||
            })}
 | 
			
		||||
          />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <AddDateRange>Add date range</AddDateRange>
 | 
			
		||||
        )}
 | 
			
		||||
      </DateRangeInputs>
 | 
			
		||||
      <DatePicker
 | 
			
		||||
        selected={startDate}
 | 
			
		||||
        onChange={date => setStartDate(date)}
 | 
			
		||||
        startDate={startDate}
 | 
			
		||||
        useWeekdaysShort
 | 
			
		||||
        renderCustomHeader={({
 | 
			
		||||
          date,
 | 
			
		||||
          changeYear,
 | 
			
		||||
          changeMonth,
 | 
			
		||||
          decreaseMonth,
 | 
			
		||||
          increaseMonth,
 | 
			
		||||
          prevMonthButtonDisabled,
 | 
			
		||||
          nextMonthButtonDisabled,
 | 
			
		||||
        }) => (
 | 
			
		||||
          <HeaderActions>
 | 
			
		||||
            <HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
 | 
			
		||||
              Prev
 | 
			
		||||
            </HeaderButton>
 | 
			
		||||
            <HeaderSelectLabel>
 | 
			
		||||
              {months[date.getMonth()]}
 | 
			
		||||
              <HeaderSelect
 | 
			
		||||
                value={months[getMonth(date)]}
 | 
			
		||||
                onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
 | 
			
		||||
              >
 | 
			
		||||
                {months.map(option => (
 | 
			
		||||
                  <option key={option} value={option}>
 | 
			
		||||
                    {option}
 | 
			
		||||
                  </option>
 | 
			
		||||
                ))}
 | 
			
		||||
              </HeaderSelect>
 | 
			
		||||
            </HeaderSelectLabel>
 | 
			
		||||
            <HeaderSelectLabel>
 | 
			
		||||
              {date.getFullYear()}
 | 
			
		||||
              <HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}>
 | 
			
		||||
                {years.map(option => (
 | 
			
		||||
                  <option key={option} value={option}>
 | 
			
		||||
                    {option}
 | 
			
		||||
                  </option>
 | 
			
		||||
                ))}
 | 
			
		||||
              </HeaderSelect>
 | 
			
		||||
            </HeaderSelectLabel>
 | 
			
		||||
        </FormField>
 | 
			
		||||
        <FormField>
 | 
			
		||||
          <Controller
 | 
			
		||||
            control={control}
 | 
			
		||||
            defaultValue={now.toDate()}
 | 
			
		||||
            name="endTime"
 | 
			
		||||
            render={({ onChange, onBlur, value }) => (
 | 
			
		||||
              <DatePicker
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                selected={value}
 | 
			
		||||
                onBlur={onBlur}
 | 
			
		||||
                showTimeSelect
 | 
			
		||||
                showTimeSelectOnly
 | 
			
		||||
                timeIntervals={15}
 | 
			
		||||
                timeCaption="Time"
 | 
			
		||||
                dateFormat="h:mm aa"
 | 
			
		||||
                customInput={<CustomTimeInput />}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          />
 | 
			
		||||
        </FormField>
 | 
			
		||||
        <DueDatePickerWrapper>
 | 
			
		||||
          <DatePicker
 | 
			
		||||
            useWeekdaysShort
 | 
			
		||||
            renderCustomHeader={({
 | 
			
		||||
              date,
 | 
			
		||||
              changeYear,
 | 
			
		||||
              changeMonth,
 | 
			
		||||
              decreaseMonth,
 | 
			
		||||
              increaseMonth,
 | 
			
		||||
              prevMonthButtonDisabled,
 | 
			
		||||
              nextMonthButtonDisabled,
 | 
			
		||||
            }) => (
 | 
			
		||||
              <HeaderActions>
 | 
			
		||||
                <HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
 | 
			
		||||
                  Prev
 | 
			
		||||
                </HeaderButton>
 | 
			
		||||
                <HeaderSelectLabel>
 | 
			
		||||
                  {months[date.getMonth()]}
 | 
			
		||||
                  <HeaderSelect
 | 
			
		||||
                    value={getYear(date)}
 | 
			
		||||
                    onChange={({ target: { value } }) => changeYear(parseInt(value, 10))}
 | 
			
		||||
                  >
 | 
			
		||||
                    {years.map(option => (
 | 
			
		||||
                      <option key={option} value={option}>
 | 
			
		||||
                        {option}
 | 
			
		||||
                      </option>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </HeaderSelect>
 | 
			
		||||
                </HeaderSelectLabel>
 | 
			
		||||
                <HeaderSelectLabel>
 | 
			
		||||
                  {date.getFullYear()}
 | 
			
		||||
                  <HeaderSelect
 | 
			
		||||
                    value={months[getMonth(date)]}
 | 
			
		||||
                    onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
 | 
			
		||||
                  >
 | 
			
		||||
                    {months.map(option => (
 | 
			
		||||
                      <option key={option} value={option}>
 | 
			
		||||
                        {option}
 | 
			
		||||
                      </option>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </HeaderSelect>
 | 
			
		||||
                </HeaderSelectLabel>
 | 
			
		||||
 | 
			
		||||
            <HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
 | 
			
		||||
              Next
 | 
			
		||||
            </HeaderButton>
 | 
			
		||||
          </HeaderActions>
 | 
			
		||||
        )}
 | 
			
		||||
        inline
 | 
			
		||||
      />
 | 
			
		||||
      <ActionsSeparator />
 | 
			
		||||
      {hasTime && (
 | 
			
		||||
        <ActionsWrapper>
 | 
			
		||||
          <ActionClock width={16} height={16} />
 | 
			
		||||
          <ActionLabel>Due Time</ActionLabel>
 | 
			
		||||
          <DatePicker
 | 
			
		||||
                <HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
 | 
			
		||||
                  Next
 | 
			
		||||
                </HeaderButton>
 | 
			
		||||
              </HeaderActions>
 | 
			
		||||
            )}
 | 
			
		||||
            selected={startDate}
 | 
			
		||||
            inline
 | 
			
		||||
            onChange={date => {
 | 
			
		||||
              setStartDate(date);
 | 
			
		||||
            }}
 | 
			
		||||
            showTimeSelect
 | 
			
		||||
            showTimeSelectOnly
 | 
			
		||||
            timeIntervals={15}
 | 
			
		||||
            timeCaption="Time"
 | 
			
		||||
            dateFormat="h:mm aa"
 | 
			
		||||
          />
 | 
			
		||||
          <ActionIcon onClick={() => enableTime(false)}>
 | 
			
		||||
            <Cross width={16} height={16} />
 | 
			
		||||
          </ActionIcon>
 | 
			
		||||
        </ActionsWrapper>
 | 
			
		||||
      )}
 | 
			
		||||
      <ActionsWrapper>
 | 
			
		||||
        {!hasTime && (
 | 
			
		||||
          <ActionIcon
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              if (startDate === null) {
 | 
			
		||||
                const today = new Date();
 | 
			
		||||
                today.setHours(12, 30, 0);
 | 
			
		||||
                setStartDate(today);
 | 
			
		||||
              if (date) {
 | 
			
		||||
                setStartDate(date);
 | 
			
		||||
              }
 | 
			
		||||
              enableTime(true);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </DueDatePickerWrapper>
 | 
			
		||||
        <ActionWrapper>
 | 
			
		||||
          <ConfirmAddDueDate type="submit" onClick={NOOP}>
 | 
			
		||||
            Save
 | 
			
		||||
          </ConfirmAddDueDate>
 | 
			
		||||
          <RemoveDueDate
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            color="danger"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              onRemoveDueDate(task);
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Clock width={16} height={16} />
 | 
			
		||||
          </ActionIcon>
 | 
			
		||||
        )}
 | 
			
		||||
        <ClearButton onClick={() => setStartDate(null)}>{hasTime ? 'Clear all' : 'Clear'}</ClearButton>
 | 
			
		||||
      </ActionsWrapper>
 | 
			
		||||
            Remove
 | 
			
		||||
          </RemoveDueDate>
 | 
			
		||||
        </ActionWrapper>
 | 
			
		||||
      </Form>
 | 
			
		||||
    </Wrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								frontend/src/shared/components/Input/Input.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/src/shared/components/Input/Input.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import styled, { ThemeProvider } from 'styled-components';
 | 
			
		||||
import { User } from 'shared/icons';
 | 
			
		||||
 | 
			
		||||
import Input from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Input,
 | 
			
		||||
  title: 'Input',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Wrapper = styled.div`
 | 
			
		||||
  background: ${props => props.theme.colors.bg.primary};
 | 
			
		||||
  padding: 45px;
 | 
			
		||||
  margin: 25px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <ThemeProvider theme={theme}>
 | 
			
		||||
        <Wrapper>
 | 
			
		||||
          <Input label="Label placeholder" />
 | 
			
		||||
          <Input width="100%" placeholder="Placeholder" />
 | 
			
		||||
          <Input icon={<User width={20} height={20} />} width="100%" placeholder="Placeholder" />
 | 
			
		||||
        </Wrapper>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										146
									
								
								frontend/src/shared/components/List/List.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								frontend/src/shared/components/List/List.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
			
		||||
import React, { createRef } from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import Card from 'shared/components/Card';
 | 
			
		||||
import CardComposer from 'shared/components/CardComposer';
 | 
			
		||||
import LabelColors from 'shared/constants/labelColors';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import List, { ListCards } from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: List,
 | 
			
		||||
  title: 'List',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const labelData: Array<ProjectLabel> = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 'development',
 | 
			
		||||
    name: 'Development',
 | 
			
		||||
    createdDate: new Date().toString(),
 | 
			
		||||
    labelColor: {
 | 
			
		||||
      id: '1',
 | 
			
		||||
      colorHex: LabelColors.BLUE,
 | 
			
		||||
      name: 'blue',
 | 
			
		||||
      position: 1,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const createCard = () => {
 | 
			
		||||
  const $ref = createRef<HTMLDivElement>();
 | 
			
		||||
  return (
 | 
			
		||||
    <Card
 | 
			
		||||
      taskID="1"
 | 
			
		||||
      taskGroupID="1"
 | 
			
		||||
      description="hello!"
 | 
			
		||||
      ref={$ref}
 | 
			
		||||
      title="Hello, world"
 | 
			
		||||
      onClick={action('on click')}
 | 
			
		||||
      onContextMenu={action('on context click')}
 | 
			
		||||
      watched
 | 
			
		||||
      labels={labelData}
 | 
			
		||||
      checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
      dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <List
 | 
			
		||||
      id=""
 | 
			
		||||
      name="General"
 | 
			
		||||
      isComposerOpen={false}
 | 
			
		||||
      onSaveName={action('on save name')}
 | 
			
		||||
      onOpenComposer={action('on open composer')}
 | 
			
		||||
      onExtraMenuOpen={action('extra menu open')}
 | 
			
		||||
    >
 | 
			
		||||
      <ListCards>
 | 
			
		||||
        <CardComposer onClose={NOOP} onCreateCard={NOOP} isOpen={false} />
 | 
			
		||||
      </ListCards>
 | 
			
		||||
    </List>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WithCardComposer = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <List
 | 
			
		||||
      id="1"
 | 
			
		||||
      name="General"
 | 
			
		||||
      isComposerOpen
 | 
			
		||||
      onSaveName={action('on save name')}
 | 
			
		||||
      onOpenComposer={action('on open composer')}
 | 
			
		||||
      onExtraMenuOpen={action('extra menu open')}
 | 
			
		||||
    >
 | 
			
		||||
      <ListCards>
 | 
			
		||||
        <CardComposer onClose={NOOP} onCreateCard={NOOP} isOpen />
 | 
			
		||||
      </ListCards>
 | 
			
		||||
    </List>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WithCard = () => {
 | 
			
		||||
  const $cardRef: any = createRef();
 | 
			
		||||
  return (
 | 
			
		||||
    <List
 | 
			
		||||
      id="1"
 | 
			
		||||
      name="General"
 | 
			
		||||
      isComposerOpen={false}
 | 
			
		||||
      onSaveName={action('on save name')}
 | 
			
		||||
      onOpenComposer={action('on open composer')}
 | 
			
		||||
      onExtraMenuOpen={action('extra menu open')}
 | 
			
		||||
    >
 | 
			
		||||
      <ListCards>
 | 
			
		||||
        <Card
 | 
			
		||||
          taskID="1"
 | 
			
		||||
          taskGroupID="1"
 | 
			
		||||
          description="hello!"
 | 
			
		||||
          ref={$cardRef}
 | 
			
		||||
          title="Hello, world"
 | 
			
		||||
          onClick={action('on click')}
 | 
			
		||||
          onContextMenu={action('on context click')}
 | 
			
		||||
          watched
 | 
			
		||||
          labels={labelData}
 | 
			
		||||
          checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
          dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
        />
 | 
			
		||||
        <CardComposer onClose={NOOP} onCreateCard={NOOP} isOpen={false} />
 | 
			
		||||
      </ListCards>
 | 
			
		||||
    </List>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
export const WithCardAndComposer = () => {
 | 
			
		||||
  const $cardRef: any = createRef();
 | 
			
		||||
  return (
 | 
			
		||||
    <List
 | 
			
		||||
      id="1"
 | 
			
		||||
      name="General"
 | 
			
		||||
      isComposerOpen
 | 
			
		||||
      onSaveName={action('on save name')}
 | 
			
		||||
      onOpenComposer={action('on open composer')}
 | 
			
		||||
      onExtraMenuOpen={action('extra menu open')}
 | 
			
		||||
    >
 | 
			
		||||
      <ListCards>
 | 
			
		||||
        <Card
 | 
			
		||||
          taskID="1"
 | 
			
		||||
          taskGroupID="1"
 | 
			
		||||
          description="hello!"
 | 
			
		||||
          ref={$cardRef}
 | 
			
		||||
          title="Hello, world"
 | 
			
		||||
          onClick={action('on click')}
 | 
			
		||||
          onContextMenu={action('on context click')}
 | 
			
		||||
          watched
 | 
			
		||||
          labels={labelData}
 | 
			
		||||
          checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
          dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
        />
 | 
			
		||||
        <CardComposer onClose={NOOP} onCreateCard={NOOP} isOpen />
 | 
			
		||||
      </ListCards>
 | 
			
		||||
    </List>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										104
									
								
								frontend/src/shared/components/Lists/Lists.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								frontend/src/shared/components/Lists/Lists.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
import Lists from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Lists,
 | 
			
		||||
  title: 'Lists',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: theme.colors.bg.secondary, default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const initialListsData = {
 | 
			
		||||
  columns: {
 | 
			
		||||
    'column-1': {
 | 
			
		||||
      taskGroupID: 'column-1',
 | 
			
		||||
      name: 'General',
 | 
			
		||||
      taskIds: ['task-3', 'task-4', 'task-1', 'task-2'],
 | 
			
		||||
      position: 1,
 | 
			
		||||
      tasks: [],
 | 
			
		||||
    },
 | 
			
		||||
    'column-2': {
 | 
			
		||||
      taskGroupID: 'column-2',
 | 
			
		||||
      name: 'Development',
 | 
			
		||||
      taskIds: [],
 | 
			
		||||
      position: 2,
 | 
			
		||||
      tasks: [],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  tasks: {
 | 
			
		||||
    'task-1': {
 | 
			
		||||
      taskID: 'task-1',
 | 
			
		||||
      taskGroup: { taskGroupID: 'column-1' },
 | 
			
		||||
      name: 'Create roadmap',
 | 
			
		||||
      position: 2,
 | 
			
		||||
      labels: [],
 | 
			
		||||
    },
 | 
			
		||||
    'task-2': {
 | 
			
		||||
      taskID: 'task-2',
 | 
			
		||||
      taskGroup: { taskGroupID: 'column-1' },
 | 
			
		||||
      position: 1,
 | 
			
		||||
      name: 'Create authentication',
 | 
			
		||||
      labels: [],
 | 
			
		||||
    },
 | 
			
		||||
    'task-3': {
 | 
			
		||||
      taskID: 'task-3',
 | 
			
		||||
      taskGroup: { taskGroupID: 'column-1' },
 | 
			
		||||
      position: 3,
 | 
			
		||||
      name: 'Create login',
 | 
			
		||||
      labels: [],
 | 
			
		||||
    },
 | 
			
		||||
    'task-4': {
 | 
			
		||||
      taskID: 'task-4',
 | 
			
		||||
      taskGroup: { taskGroupID: 'column-1' },
 | 
			
		||||
      position: 4,
 | 
			
		||||
      name: 'Create plugins',
 | 
			
		||||
      labels: [],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  const [listsData, setListsData] = useState(initialListsData);
 | 
			
		||||
  const onCardDrop = (droppedTask: Task) => {
 | 
			
		||||
    const newState = {
 | 
			
		||||
      ...listsData,
 | 
			
		||||
      tasks: {
 | 
			
		||||
        ...listsData.tasks,
 | 
			
		||||
        [droppedTask.id]: droppedTask,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    setListsData(newState);
 | 
			
		||||
  };
 | 
			
		||||
  const onListDrop = (droppedColumn: any) => {
 | 
			
		||||
    const newState = {
 | 
			
		||||
      ...listsData,
 | 
			
		||||
      columns: {
 | 
			
		||||
        ...listsData.columns,
 | 
			
		||||
        [droppedColumn.taskGroupID]: droppedColumn,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    setListsData(newState);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Lists
 | 
			
		||||
      taskGroups={[]}
 | 
			
		||||
      onTaskClick={action('card click')}
 | 
			
		||||
      onQuickEditorOpen={action('card composer open')}
 | 
			
		||||
      onCreateTask={action('card create')}
 | 
			
		||||
      onTaskDrop={onCardDrop}
 | 
			
		||||
      onTaskGroupDrop={onListDrop}
 | 
			
		||||
      onChangeTaskGroupName={action('change group name')}
 | 
			
		||||
      cardLabelVariant="large"
 | 
			
		||||
      onCardLabelClick={action('label click')}
 | 
			
		||||
      onCreateTaskGroup={action('create list')}
 | 
			
		||||
      onExtraMenuOpen={action('extra menu open')}
 | 
			
		||||
      onCardMemberClick={action('card member click')}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
 | 
			
		||||
export const Container = styled.div`
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  overflow-x: auto;
 | 
			
		||||
  overflow-y: hidden;
 | 
			
		||||
 | 
			
		||||
  ::-webkit-scrollbar {
 | 
			
		||||
    height: 10px;
 | 
			
		||||
 
 | 
			
		||||
@@ -391,16 +391,16 @@ const SimpleLists: React.FC<SimpleProps> = ({
 | 
			
		||||
                      </Draggable>
 | 
			
		||||
                    );
 | 
			
		||||
                  })}
 | 
			
		||||
                <AddList
 | 
			
		||||
                  onSave={listName => {
 | 
			
		||||
                    onCreateTaskGroup(listName);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
                {provided.placeholder}
 | 
			
		||||
              </Container>
 | 
			
		||||
            )}
 | 
			
		||||
          </Droppable>
 | 
			
		||||
        </DragDropContext>
 | 
			
		||||
        <AddList
 | 
			
		||||
          onSave={listName => {
 | 
			
		||||
            onCreateTaskGroup(listName);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </BoardWrapper>
 | 
			
		||||
    </BoardContainer>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import LoadingSpinner from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: LoadingSpinner,
 | 
			
		||||
  title: 'Login',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
      { name: 'gray', value: '#cdd3e1', default: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <LoadingSpinner />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										67
									
								
								frontend/src/shared/components/Login/Login.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								frontend/src/shared/components/Login/Login.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import Login from '.';
 | 
			
		||||
 | 
			
		||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Login,
 | 
			
		||||
  title: 'Login',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
      { name: 'gray', value: '#cdd3e1', default: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Container = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width: 100vw;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const LoginWrapper = styled.div`
 | 
			
		||||
  width: 60%;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Container>
 | 
			
		||||
        <LoginWrapper>
 | 
			
		||||
          <Login onSubmit={action('on submit')} />
 | 
			
		||||
        </LoginWrapper>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const WithSubmission = () => {
 | 
			
		||||
  const onSubmit = async (data: LoginFormData, setComplete: (val: boolean) => void, setError: any) => {
 | 
			
		||||
    await sleep(2000);
 | 
			
		||||
    if (data.username !== 'test' || data.password !== 'test') {
 | 
			
		||||
      setError('username', 'invalid', 'Invalid username');
 | 
			
		||||
      setError('password', 'invalid', 'Invalid password');
 | 
			
		||||
    }
 | 
			
		||||
    setComplete(true);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Container>
 | 
			
		||||
        <LoginWrapper>
 | 
			
		||||
          <Login onSubmit={onSubmit} />
 | 
			
		||||
        </LoginWrapper>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import MemberManager from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: MemberManager,
 | 
			
		||||
  title: 'MemberManager',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return <MemberManager availableMembers={[]} activeMembers={[]} onMemberChange={action('member change')} />;
 | 
			
		||||
};
 | 
			
		||||
@@ -20,14 +20,14 @@ export const MemberManagerSearch = styled(TextareaAutosize)`
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
 | 
			
		||||
  background: ${props => props.theme.colors.bg.secondary};
 | 
			
		||||
  background: ${props => props.theme.colors.bgColor.secondary};
 | 
			
		||||
  outline: none;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  border-color: ${props => props.theme.colors.border};
 | 
			
		||||
 | 
			
		||||
  &:focus {
 | 
			
		||||
    box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
 | 
			
		||||
    background: ${props => mixin.darken(props.theme.colors.bg.secondary, 0.15)};
 | 
			
		||||
    background: ${props => mixin.darken(props.theme.colors.bgColor.secondary, 0.15)};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								frontend/src/shared/components/Modal/Modal.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								frontend/src/shared/components/Modal/Modal.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import Modal from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Modal,
 | 
			
		||||
  title: 'Modal',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
      { name: 'gray', value: '#cdd3e1', default: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Modal
 | 
			
		||||
        width={1040}
 | 
			
		||||
        onClose={action('on close')}
 | 
			
		||||
        renderContent={() => {
 | 
			
		||||
          return <h1>Hello!</h1>;
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										40
									
								
								frontend/src/shared/components/Navbar/Navbar.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								frontend/src/shared/components/Navbar/Navbar.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import { Home } from 'shared/icons';
 | 
			
		||||
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Navbar,
 | 
			
		||||
  title: 'Navbar',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#cdd3e1' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
const MainContent = styled.div`
 | 
			
		||||
  padding: 0 0 50px 80px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Navbar>
 | 
			
		||||
        <PrimaryLogo />
 | 
			
		||||
        <ButtonContainer>
 | 
			
		||||
          <ActionButton name="Home">
 | 
			
		||||
            <Home width={28} height={28} />
 | 
			
		||||
          </ActionButton>
 | 
			
		||||
          <ActionButton name="Home">
 | 
			
		||||
            <Home width={28} height={28} />
 | 
			
		||||
          </ActionButton>
 | 
			
		||||
        </ButtonContainer>
 | 
			
		||||
      </Navbar>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import NewProject from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: NewProject,
 | 
			
		||||
  title: 'NewProject',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <NewProject
 | 
			
		||||
        initialTeamID={null}
 | 
			
		||||
        onCreateProject={action('create project')}
 | 
			
		||||
        teams={[{ name: 'General', id: 'general', createdAt: '' }]}
 | 
			
		||||
        onClose={NOOP}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -263,7 +263,7 @@ const NewProject: React.FC<NewProjectProps> = ({ initialTeamID, teams, onClose,
 | 
			
		||||
                  onChange={(e: any) => {
 | 
			
		||||
                    setTeam(e.value);
 | 
			
		||||
                  }}
 | 
			
		||||
                  value={options.find(d => d.value === team)}
 | 
			
		||||
                  value={options.filter(d => d.value === team)}
 | 
			
		||||
                  styles={colourStyles}
 | 
			
		||||
                  classNamePrefix="teamSelect"
 | 
			
		||||
                  options={options}
 | 
			
		||||
 
 | 
			
		||||
@@ -87,12 +87,6 @@ export const HeaderTitle = styled.span`
 | 
			
		||||
 | 
			
		||||
export const Content = styled.div`
 | 
			
		||||
  max-height: 632px;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
  &::-webkit-scrollbar-track-piece {
 | 
			
		||||
    background: ${props => props.theme.colors.bg.primary};
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const LabelSearch = styled(ControlledInput)`
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import {
 | 
			
		||||
} from './Styles';
 | 
			
		||||
 | 
			
		||||
function getPopupOptions(options?: PopupOptions) {
 | 
			
		||||
  const popupOptions: PopupOptionsInternal = {
 | 
			
		||||
  const popupOptions = {
 | 
			
		||||
    borders: true,
 | 
			
		||||
    diamondColor: theme.colors.bg.secondary,
 | 
			
		||||
    targetPadding: '10px',
 | 
			
		||||
@@ -40,9 +40,6 @@ function getPopupOptions(options?: PopupOptions) {
 | 
			
		||||
    if (options.diamondColor) {
 | 
			
		||||
      popupOptions.diamondColor = options.diamondColor;
 | 
			
		||||
    }
 | 
			
		||||
    if (options.onClose) {
 | 
			
		||||
      popupOptions.onClose = options.onClose;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return popupOptions;
 | 
			
		||||
}
 | 
			
		||||
@@ -139,7 +136,6 @@ type PopupOptionsInternal = {
 | 
			
		||||
  targetPadding: string;
 | 
			
		||||
  diamondColor: string;
 | 
			
		||||
  showDiamond: boolean;
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type PopupOptions = {
 | 
			
		||||
@@ -148,7 +144,6 @@ type PopupOptions = {
 | 
			
		||||
  width?: number | null;
 | 
			
		||||
  borders?: boolean | null;
 | 
			
		||||
  diamondColor?: string | null;
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
};
 | 
			
		||||
const defaultState = {
 | 
			
		||||
  isOpen: false,
 | 
			
		||||
@@ -244,12 +239,7 @@ export const PopupProvider: React.FC = ({ children }) => {
 | 
			
		||||
            top={currentState.top}
 | 
			
		||||
            targetPadding={currentState.options.targetPadding}
 | 
			
		||||
            left={currentState.left}
 | 
			
		||||
            onClose={() => {
 | 
			
		||||
              if (currentState.options && currentState.options.onClose) {
 | 
			
		||||
                currentState.options.onClose();
 | 
			
		||||
              }
 | 
			
		||||
              setState(defaultState);
 | 
			
		||||
            }}
 | 
			
		||||
            onClose={() => setState(defaultState)}
 | 
			
		||||
            width={currentState.options.width}
 | 
			
		||||
          >
 | 
			
		||||
            {currentState.content}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import ProjectGridItem from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: ProjectGridItem,
 | 
			
		||||
  title: 'ProjectGridItem',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const projectsData = [
 | 
			
		||||
  { taskGroups: [], teamTitle: 'Personal', projectID: 'aaaa', name: 'Taskcafé', color: '#aa62e3' },
 | 
			
		||||
  { taskGroups: [], teamTitle: 'Personal', projectID: 'bbbb', name: 'Editorial Calender', color: '#aa62e3' },
 | 
			
		||||
  { taskGroups: [], teamTitle: 'Personal', projectID: 'cccc', name: 'New Blog', color: '#aa62e3' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const Container = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width: 100vw;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ProjectsWrapper = styled.div`
 | 
			
		||||
  width: 60%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Container>
 | 
			
		||||
      <ProjectsWrapper>
 | 
			
		||||
        {projectsData.map(project => (
 | 
			
		||||
          <ProjectGridItem project={project} />
 | 
			
		||||
        ))}
 | 
			
		||||
      </ProjectsWrapper>
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,99 @@
 | 
			
		||||
import React, { createRef, useState } from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import Card from 'shared/components/Card';
 | 
			
		||||
import CardComposer from 'shared/components/CardComposer';
 | 
			
		||||
import LabelColors from 'shared/constants/labelColors';
 | 
			
		||||
import List, { ListCards } from 'shared/components/List';
 | 
			
		||||
import QuickCardEditor from 'shared/components/QuickCardEditor';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: QuickCardEditor,
 | 
			
		||||
  title: 'QuickCardEditor',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const labelData: Array<TaskLabel> = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 'development',
 | 
			
		||||
    assignedDate: new Date().toString(),
 | 
			
		||||
    projectLabel: {
 | 
			
		||||
      id: 'development',
 | 
			
		||||
      name: 'Development',
 | 
			
		||||
      createdDate: 'date',
 | 
			
		||||
      labelColor: {
 | 
			
		||||
        id: 'label-color-blue',
 | 
			
		||||
        colorHex: LabelColors.BLUE,
 | 
			
		||||
        name: 'blue',
 | 
			
		||||
        position: 1,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  const $cardRef: any = createRef();
 | 
			
		||||
  const task: Task = {
 | 
			
		||||
    id: 'task',
 | 
			
		||||
    name: 'Hello, world!',
 | 
			
		||||
    position: 1,
 | 
			
		||||
    labels: labelData,
 | 
			
		||||
    taskGroup: {
 | 
			
		||||
      id: '1',
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
  const [isEditorOpen, setEditorOpen] = useState(false);
 | 
			
		||||
  const [target, setTarget] = useState<null | React.RefObject<HTMLElement>>(null);
 | 
			
		||||
  const [top, setTop] = useState(0);
 | 
			
		||||
  const [left, setLeft] = useState(0);
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {isEditorOpen && target && (
 | 
			
		||||
        <QuickCardEditor
 | 
			
		||||
          task={task}
 | 
			
		||||
          onCloseEditor={() => setEditorOpen(false)}
 | 
			
		||||
          onEditCard={action('edit card')}
 | 
			
		||||
          onOpenLabelsPopup={action('open popup')}
 | 
			
		||||
          onOpenDueDatePopup={action('open popup')}
 | 
			
		||||
          onOpenMembersPopup={action('open popup')}
 | 
			
		||||
          onToggleComplete={action('complete')}
 | 
			
		||||
          onArchiveCard={action('archive card')}
 | 
			
		||||
          target={target}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <List
 | 
			
		||||
        id="1"
 | 
			
		||||
        name="General"
 | 
			
		||||
        isComposerOpen={false}
 | 
			
		||||
        onSaveName={action('on save name')}
 | 
			
		||||
        onOpenComposer={action('on open composer')}
 | 
			
		||||
        onExtraMenuOpen={NOOP}
 | 
			
		||||
      >
 | 
			
		||||
        <ListCards>
 | 
			
		||||
          <Card
 | 
			
		||||
            taskID="1"
 | 
			
		||||
            taskGroupID="1"
 | 
			
		||||
            description="hello!"
 | 
			
		||||
            ref={$cardRef}
 | 
			
		||||
            title={task.name}
 | 
			
		||||
            onClick={action('on click')}
 | 
			
		||||
            onContextMenu={() => {
 | 
			
		||||
              setTarget($cardRef);
 | 
			
		||||
              setEditorOpen(true);
 | 
			
		||||
            }}
 | 
			
		||||
            watched
 | 
			
		||||
            labels={labelData.map(l => l.projectLabel)}
 | 
			
		||||
            checklists={{ complete: 1, total: 4 }}
 | 
			
		||||
            dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
 | 
			
		||||
          />
 | 
			
		||||
          <CardComposer onClose={NOOP} onCreateCard={NOOP} isOpen={false} />
 | 
			
		||||
        </ListCards>
 | 
			
		||||
      </List>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -84,58 +84,6 @@ export const colourStyles = {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const editorColourStyles = {
 | 
			
		||||
  ...colourStyles,
 | 
			
		||||
  input: (styles: any) => ({
 | 
			
		||||
    ...styles,
 | 
			
		||||
    color: '#000',
 | 
			
		||||
  }),
 | 
			
		||||
  singleValue: (styles: any) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...styles,
 | 
			
		||||
      color: '#000',
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  menu: (styles: any) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...styles,
 | 
			
		||||
      backgroundColor: '#fff',
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  indicatorsContainer: (styles: any) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...styles,
 | 
			
		||||
      display: 'none',
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  container: (styles: any) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...styles,
 | 
			
		||||
      display: 'flex',
 | 
			
		||||
      flex: '1 1',
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  control: (styles: any, data: any) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...styles,
 | 
			
		||||
      flex: '1 1',
 | 
			
		||||
      backgroundColor: 'transparent',
 | 
			
		||||
      boxShadow: 'none',
 | 
			
		||||
      borderRadius: '0',
 | 
			
		||||
      minHeight: '35px',
 | 
			
		||||
      border: '0',
 | 
			
		||||
      ':hover': {
 | 
			
		||||
        boxShadow: 'none',
 | 
			
		||||
        borderRadius: '0',
 | 
			
		||||
      },
 | 
			
		||||
      ':active': {
 | 
			
		||||
        boxShadow: 'none',
 | 
			
		||||
        borderRadius: '0',
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const InputLabel = styled.span<{ width: string }>`
 | 
			
		||||
width: ${props => props.width};
 | 
			
		||||
padding-left: 0.7rem;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								frontend/src/shared/components/Settings/Settings.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/src/shared/components/Settings/Settings.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import React, { createRef, useState } from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import Settings from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Settings,
 | 
			
		||||
  title: 'Settings',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff', default: true },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
const profile = {
 | 
			
		||||
  id: '1',
 | 
			
		||||
  fullName: 'Jordan Knott',
 | 
			
		||||
  username: 'jordanthedev',
 | 
			
		||||
  profileIcon: { url: '/uploads/headshot.png', bgColor: '#000', initials: 'JK' },
 | 
			
		||||
};
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Settings
 | 
			
		||||
        profile={profile}
 | 
			
		||||
        onChangeUserInfo={action('change user info')}
 | 
			
		||||
        onResetPassword={action('reset password')}
 | 
			
		||||
        onProfileAvatarRemove={action('remove')}
 | 
			
		||||
        onProfileAvatarChange={action('profile avatar change')}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										27
									
								
								frontend/src/shared/components/Sidebar/Sidebar.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/shared/components/Sidebar/Sidebar.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import Navbar from 'shared/components/Navbar';
 | 
			
		||||
import Sidebar from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Sidebar,
 | 
			
		||||
  title: 'Sidebar',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
      { name: 'gray', value: '#cdd3e1', default: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Navbar />
 | 
			
		||||
      <Sidebar />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										25
									
								
								frontend/src/shared/components/Tabs/Tabs.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/shared/components/Tabs/Tabs.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import React, { useRef } from 'react';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import Tabs from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: Tabs,
 | 
			
		||||
  title: 'Tabs',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8', default: true },
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Tabs />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import { TaskActivityData, ActivityType } from 'shared/generated/graphql';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
 | 
			
		||||
type ActivityMessageProps = {
 | 
			
		||||
  type: ActivityType;
 | 
			
		||||
  data: Array<TaskActivityData>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getVariable(data: Array<TaskActivityData>, name: string) {
 | 
			
		||||
  const target = data.find(d => d.name === name);
 | 
			
		||||
  return target ? target.value : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderDate(timestamp: string | null) {
 | 
			
		||||
  if (timestamp) {
 | 
			
		||||
    return dayjs(timestamp).format('MMM D [at] h:mm A');
 | 
			
		||||
  }
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ActivityMessage: React.FC<ActivityMessageProps> = ({ type, data }) => {
 | 
			
		||||
  let message = '';
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case ActivityType.TaskAdded:
 | 
			
		||||
      message = `added this task to ${getVariable(data, 'TaskGroup')}`;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActivityType.TaskMoved:
 | 
			
		||||
      message = `moved this task from ${getVariable(data, 'PrevTaskGroup')} to ${getVariable(data, 'CurTaskGroup')}`;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActivityType.TaskDueDateAdded:
 | 
			
		||||
      message = `set this task to be due ${renderDate(getVariable(data, 'DueDate'))}`;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActivityType.TaskDueDateRemoved:
 | 
			
		||||
      message = `removed the due date from this task`;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActivityType.TaskDueDateChanged:
 | 
			
		||||
      message = `changed the due date of this task to ${renderDate(getVariable(data, 'CurDueDate'))}`;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActivityType.TaskMarkedComplete:
 | 
			
		||||
      message = `marked this task complete`;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActivityType.TaskMarkedIncomplete:
 | 
			
		||||
      message = `marked this task incomplete`;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      message = '<unknown type>';
 | 
			
		||||
  }
 | 
			
		||||
  return <>{message}</>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ActivityMessage;
 | 
			
		||||
@@ -1,135 +0,0 @@
 | 
			
		||||
import React, { useRef, useState, useEffect } from 'react';
 | 
			
		||||
import { usePopup } from 'shared/components/PopupMenu';
 | 
			
		||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
			
		||||
import { At, Paperclip, Smile } from 'shared/icons';
 | 
			
		||||
import { Picker, Emoji } from 'emoji-mart';
 | 
			
		||||
import Task from 'shared/icons/Task';
 | 
			
		||||
import {
 | 
			
		||||
  CommentTextArea,
 | 
			
		||||
  CommentEditorContainer,
 | 
			
		||||
  CommentEditorActions,
 | 
			
		||||
  CommentEditorActionIcon,
 | 
			
		||||
  CommentEditorSaveButton,
 | 
			
		||||
  CommentProfile,
 | 
			
		||||
  CommentInnerWrapper,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
 | 
			
		||||
type CommentCreatorProps = {
 | 
			
		||||
  me?: TaskUser;
 | 
			
		||||
  autoFocus?: boolean;
 | 
			
		||||
  onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
 | 
			
		||||
  message?: string | null;
 | 
			
		||||
  onCreateComment: (message: string) => void;
 | 
			
		||||
  onCancelEdit?: () => void;
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CommentCreator: React.FC<CommentCreatorProps> = ({
 | 
			
		||||
  me,
 | 
			
		||||
  disabled = false,
 | 
			
		||||
  message,
 | 
			
		||||
  onMemberProfile,
 | 
			
		||||
  onCreateComment,
 | 
			
		||||
  onCancelEdit,
 | 
			
		||||
  autoFocus = false,
 | 
			
		||||
}) => {
 | 
			
		||||
  const $commentWrapper = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $comment = useRef<HTMLTextAreaElement>(null);
 | 
			
		||||
  const $emoji = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $emojiCart = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const [comment, setComment] = useState(message ?? '');
 | 
			
		||||
  const [showCommentActions, setShowCommentActions] = useState(autoFocus);
 | 
			
		||||
  const { showPopup, hidePopup } = usePopup();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (autoFocus && $comment && $comment.current) {
 | 
			
		||||
      $comment.current.select();
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
  useOnOutsideClick(
 | 
			
		||||
    [$commentWrapper, $emojiCart],
 | 
			
		||||
    showCommentActions,
 | 
			
		||||
    () => {
 | 
			
		||||
      if (onCancelEdit) {
 | 
			
		||||
        onCancelEdit();
 | 
			
		||||
      }
 | 
			
		||||
      setShowCommentActions(false);
 | 
			
		||||
    },
 | 
			
		||||
    null,
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <CommentInnerWrapper ref={$commentWrapper}>
 | 
			
		||||
      {me && onMemberProfile && (
 | 
			
		||||
        <CommentProfile
 | 
			
		||||
          member={me}
 | 
			
		||||
          size={32}
 | 
			
		||||
          onMemberProfile={$target => {
 | 
			
		||||
            onMemberProfile($target, me.id);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <CommentEditorContainer>
 | 
			
		||||
        <CommentTextArea
 | 
			
		||||
          showCommentActions={showCommentActions}
 | 
			
		||||
          placeholder="Write a comment..."
 | 
			
		||||
          ref={$comment}
 | 
			
		||||
          disabled={disabled}
 | 
			
		||||
          value={comment}
 | 
			
		||||
          onChange={e => setComment(e.currentTarget.value)}
 | 
			
		||||
          onFocus={() => {
 | 
			
		||||
            setShowCommentActions(true);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <CommentEditorActions visible={showCommentActions}>
 | 
			
		||||
          <CommentEditorActionIcon>
 | 
			
		||||
            <Paperclip width={12} height={12} />
 | 
			
		||||
          </CommentEditorActionIcon>
 | 
			
		||||
          <CommentEditorActionIcon>
 | 
			
		||||
            <At width={12} height={12} />
 | 
			
		||||
          </CommentEditorActionIcon>
 | 
			
		||||
          <CommentEditorActionIcon
 | 
			
		||||
            ref={$emoji}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              showPopup(
 | 
			
		||||
                $emoji,
 | 
			
		||||
                <div ref={$emojiCart}>
 | 
			
		||||
                  <Picker
 | 
			
		||||
                    onClick={emoji => {
 | 
			
		||||
                      if ($comment && $comment.current) {
 | 
			
		||||
                        const textToInsert = `${emoji.colons} `;
 | 
			
		||||
                        const cursorPosition = $comment.current.selectionStart;
 | 
			
		||||
                        const textBeforeCursorPosition = $comment.current.value.substring(0, cursorPosition);
 | 
			
		||||
                        const textAfterCursorPosition = $comment.current.value.substring(
 | 
			
		||||
                          cursorPosition,
 | 
			
		||||
                          $comment.current.value.length,
 | 
			
		||||
                        );
 | 
			
		||||
                        setComment(textBeforeCursorPosition + textToInsert + textAfterCursorPosition);
 | 
			
		||||
                      }
 | 
			
		||||
                      hidePopup();
 | 
			
		||||
                    }}
 | 
			
		||||
                    set="google"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>,
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Smile width={12} height={12} />
 | 
			
		||||
          </CommentEditorActionIcon>
 | 
			
		||||
          <CommentEditorActionIcon>
 | 
			
		||||
            <Task width={12} height={12} />
 | 
			
		||||
          </CommentEditorActionIcon>
 | 
			
		||||
          <CommentEditorSaveButton
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setShowCommentActions(false);
 | 
			
		||||
              onCreateComment(comment);
 | 
			
		||||
              setComment('');
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            Save
 | 
			
		||||
          </CommentEditorSaveButton>
 | 
			
		||||
        </CommentEditorActions>
 | 
			
		||||
      </CommentEditorContainer>
 | 
			
		||||
    </CommentInnerWrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default CommentCreator;
 | 
			
		||||
@@ -1,162 +0,0 @@
 | 
			
		||||
import React, { useState, useRef } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
  Plus,
 | 
			
		||||
  User,
 | 
			
		||||
  Trash,
 | 
			
		||||
  Paperclip,
 | 
			
		||||
  Clone,
 | 
			
		||||
  Share,
 | 
			
		||||
  Tags,
 | 
			
		||||
  Checkmark,
 | 
			
		||||
  CheckSquareOutline,
 | 
			
		||||
  At,
 | 
			
		||||
  Smile,
 | 
			
		||||
} from 'shared/icons';
 | 
			
		||||
import { toArray } from 'react-emoji-render';
 | 
			
		||||
import DOMPurify from 'dompurify';
 | 
			
		||||
import TaskAssignee from 'shared/components/TaskAssignee';
 | 
			
		||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
			
		||||
import { usePopup } from 'shared/components/PopupMenu';
 | 
			
		||||
import CommentCreator from 'shared/components/TaskDetails/CommentCreator';
 | 
			
		||||
import { AngleDown } from 'shared/icons/AngleDown';
 | 
			
		||||
import Editor from 'rich-markdown-editor';
 | 
			
		||||
import dark from 'shared/utils/editorTheme';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import ReactMarkdown from 'react-markdown';
 | 
			
		||||
import { Picker, Emoji } from 'emoji-mart';
 | 
			
		||||
import 'emoji-mart/css/emoji-mart.css';
 | 
			
		||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import Task from 'shared/icons/Task';
 | 
			
		||||
import {
 | 
			
		||||
  ActivityItemHeader,
 | 
			
		||||
  ActivityItemTimestamp,
 | 
			
		||||
  ActivityItem,
 | 
			
		||||
  ActivityItemCommentAction,
 | 
			
		||||
  ActivityItemCommentActions,
 | 
			
		||||
  TaskDetailLabel,
 | 
			
		||||
  CommentContainer,
 | 
			
		||||
  ActivityItemCommentContainer,
 | 
			
		||||
  MetaDetailContent,
 | 
			
		||||
  TaskDetailsAddLabelIcon,
 | 
			
		||||
  ActionButton,
 | 
			
		||||
  AssignUserIcon,
 | 
			
		||||
  AssignUserLabel,
 | 
			
		||||
  AssignUsersButton,
 | 
			
		||||
  AssignedUsersSection,
 | 
			
		||||
  ViewRawButton,
 | 
			
		||||
  DueDateTitle,
 | 
			
		||||
  Container,
 | 
			
		||||
  LeftSidebar,
 | 
			
		||||
  SidebarSkeleton,
 | 
			
		||||
  ContentContainer,
 | 
			
		||||
  LeftSidebarContent,
 | 
			
		||||
  LeftSidebarSection,
 | 
			
		||||
  SidebarTitle,
 | 
			
		||||
  SidebarButton,
 | 
			
		||||
  SidebarButtonText,
 | 
			
		||||
  MarkCompleteButton,
 | 
			
		||||
  HeaderContainer,
 | 
			
		||||
  HeaderLeft,
 | 
			
		||||
  HeaderInnerContainer,
 | 
			
		||||
  TaskDetailsTitleWrapper,
 | 
			
		||||
  TaskDetailsTitle,
 | 
			
		||||
  ExtraActionsSection,
 | 
			
		||||
  HeaderRight,
 | 
			
		||||
  HeaderActionIcon,
 | 
			
		||||
  EditorContainer,
 | 
			
		||||
  InnerContentContainer,
 | 
			
		||||
  DescriptionContainer,
 | 
			
		||||
  Labels,
 | 
			
		||||
  ChecklistSection,
 | 
			
		||||
  MemberList,
 | 
			
		||||
  TaskMember,
 | 
			
		||||
  TabBarSection,
 | 
			
		||||
  TabBarItem,
 | 
			
		||||
  ActivitySection,
 | 
			
		||||
  TaskDetailsEditor,
 | 
			
		||||
  ActivityItemHeaderUser,
 | 
			
		||||
  ActivityItemHeaderTitle,
 | 
			
		||||
  ActivityItemHeaderTitleName,
 | 
			
		||||
  ActivityItemComment,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
 | 
			
		||||
type TaskDetailsProps = {};
 | 
			
		||||
 | 
			
		||||
const TaskDetailsLoading: React.FC<TaskDetailsProps> = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Container>
 | 
			
		||||
      <LeftSidebar>
 | 
			
		||||
        <LeftSidebarContent>
 | 
			
		||||
          <LeftSidebarSection>
 | 
			
		||||
            <SidebarTitle>TASK GROUP</SidebarTitle>
 | 
			
		||||
            <SidebarButton loading>
 | 
			
		||||
              <SidebarSkeleton />
 | 
			
		||||
            </SidebarButton>
 | 
			
		||||
            <DueDateTitle>DUE DATE</DueDateTitle>
 | 
			
		||||
            <SidebarButton loading>
 | 
			
		||||
              <SidebarSkeleton />
 | 
			
		||||
            </SidebarButton>
 | 
			
		||||
          </LeftSidebarSection>
 | 
			
		||||
          <AssignedUsersSection>
 | 
			
		||||
            <DueDateTitle>MEMBERS</DueDateTitle>
 | 
			
		||||
            <SidebarButton loading>
 | 
			
		||||
              <SidebarSkeleton />
 | 
			
		||||
            </SidebarButton>
 | 
			
		||||
          </AssignedUsersSection>
 | 
			
		||||
          <ExtraActionsSection>
 | 
			
		||||
            <DueDateTitle>ACTIONS</DueDateTitle>
 | 
			
		||||
            <ActionButton disabled icon={<Tags width={12} height={12} />}>
 | 
			
		||||
              Labels
 | 
			
		||||
            </ActionButton>
 | 
			
		||||
            <ActionButton disabled icon={<CheckSquareOutline width={12} height={12} />}>
 | 
			
		||||
              Checklist
 | 
			
		||||
            </ActionButton>
 | 
			
		||||
            <ActionButton disabled>Cover</ActionButton>
 | 
			
		||||
          </ExtraActionsSection>
 | 
			
		||||
        </LeftSidebarContent>
 | 
			
		||||
      </LeftSidebar>
 | 
			
		||||
      <ContentContainer>
 | 
			
		||||
        <HeaderContainer>
 | 
			
		||||
          <HeaderInnerContainer>
 | 
			
		||||
            <HeaderLeft>
 | 
			
		||||
              <MarkCompleteButton disabled invert={false}>
 | 
			
		||||
                <Checkmark width={8} height={8} />
 | 
			
		||||
                <span>Mark complete</span>
 | 
			
		||||
              </MarkCompleteButton>
 | 
			
		||||
            </HeaderLeft>
 | 
			
		||||
            <HeaderRight>
 | 
			
		||||
              <HeaderActionIcon>
 | 
			
		||||
                <Paperclip width={16} height={16} />
 | 
			
		||||
              </HeaderActionIcon>
 | 
			
		||||
              <HeaderActionIcon>
 | 
			
		||||
                <Clone width={16} height={16} />
 | 
			
		||||
              </HeaderActionIcon>
 | 
			
		||||
              <HeaderActionIcon>
 | 
			
		||||
                <Share width={16} height={16} />
 | 
			
		||||
              </HeaderActionIcon>
 | 
			
		||||
              <HeaderActionIcon>
 | 
			
		||||
                <Trash width={16} height={16} />
 | 
			
		||||
              </HeaderActionIcon>
 | 
			
		||||
            </HeaderRight>
 | 
			
		||||
          </HeaderInnerContainer>
 | 
			
		||||
          <TaskDetailsTitleWrapper loading>
 | 
			
		||||
            <TaskDetailsTitle value="" disabled loading />
 | 
			
		||||
          </TaskDetailsTitleWrapper>
 | 
			
		||||
        </HeaderContainer>
 | 
			
		||||
        <InnerContentContainer>
 | 
			
		||||
          <DescriptionContainer />
 | 
			
		||||
          <TabBarSection>
 | 
			
		||||
            <TabBarItem>Activity</TabBarItem>
 | 
			
		||||
          </TabBarSection>
 | 
			
		||||
          <ActivitySection />
 | 
			
		||||
        </InnerContentContainer>
 | 
			
		||||
        <CommentContainer>
 | 
			
		||||
          <CommentCreator disabled onCreateComment={() => null} onMemberProfile={() => null} />
 | 
			
		||||
        </CommentContainer>
 | 
			
		||||
      </ContentContainer>
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TaskDetailsLoading;
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import styled, { css, keyframes } from 'styled-components';
 | 
			
		||||
import styled, { css } from 'styled-components';
 | 
			
		||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
			
		||||
import { mixin } from 'shared/utils/styles';
 | 
			
		||||
import Button from 'shared/components/Button';
 | 
			
		||||
import TaskAssignee from 'shared/components/TaskAssignee';
 | 
			
		||||
import theme from 'App/ThemeStyles';
 | 
			
		||||
 | 
			
		||||
export const Container = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
@@ -17,7 +16,7 @@ export const LeftSidebar = styled.div`
 | 
			
		||||
  background: #222740;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: boolean }>`
 | 
			
		||||
export const MarkCompleteButton = styled.button<{ invert: boolean }>`
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  border: none;
 | 
			
		||||
@@ -63,11 +62,6 @@ export const MarkCompleteButton = styled.button<{ invert: boolean; disabled?: bo
 | 
			
		||||
            color: ${props.theme.colors.success};
 | 
			
		||||
          }
 | 
			
		||||
        `}
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.invert &&
 | 
			
		||||
    css`
 | 
			
		||||
      opacity: 0.6;
 | 
			
		||||
    `}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const LeftSidebarContent = styled.div`
 | 
			
		||||
@@ -95,55 +89,24 @@ export const SidebarTitle = styled.div`
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const defaultBaseColor = theme.colors.bg.primary;
 | 
			
		||||
 | 
			
		||||
export const defaultHighlightColor = mixin.lighten(theme.colors.bg.primary, 0.25);
 | 
			
		||||
 | 
			
		||||
export const skeletonKeyframes = keyframes`
 | 
			
		||||
  0% {
 | 
			
		||||
    background-position: -200px 0;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    background-position: calc(200px + 100%) 0;
 | 
			
		||||
  }
 | 
			
		||||
  `;
 | 
			
		||||
 | 
			
		||||
export const SidebarButton = styled.div<{ loading?: boolean }>`
 | 
			
		||||
export const SidebarButton = styled.div`
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  min-height: 32px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.loading
 | 
			
		||||
      ? css`
 | 
			
		||||
          background: ${props.theme.colors.bg.primary};
 | 
			
		||||
        `
 | 
			
		||||
      : css`
 | 
			
		||||
          padding: 9px 8px 7px 8px;
 | 
			
		||||
          cursor: pointer;
 | 
			
		||||
          border-color: transparent;
 | 
			
		||||
          border-width: 1px;
 | 
			
		||||
          border-style: solid;
 | 
			
		||||
          &:hover {
 | 
			
		||||
            border-color: #414561;
 | 
			
		||||
          }
 | 
			
		||||
        `};
 | 
			
		||||
  padding: 9px 8px 7px 8px;
 | 
			
		||||
  border-color: transparent;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  outline: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const SidebarSkeleton = styled.div`
 | 
			
		||||
  background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
 | 
			
		||||
  background-size: 200px 100%;
 | 
			
		||||
  background-repeat: no-repeat;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  padding: 1px;
 | 
			
		||||
  animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    border-color: #414561;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const SidebarButtonText = styled.span`
 | 
			
		||||
@@ -178,18 +141,18 @@ export const HeaderLeft = styled.div`
 | 
			
		||||
  justify-content: flex-start;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const TaskDetailsTitleWrapper = styled.div<{ loading?: boolean }>`
 | 
			
		||||
export const TaskDetailsTitleWrapper = styled.div`
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 8px 0 4px 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  ${props => props.loading && `background: ${props.theme.colors.bg.primary};`}
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>`
 | 
			
		||||
export const TaskDetailsTitle = styled(TextareaAutosize)`
 | 
			
		||||
  padding: 9px 8px 7px 8px;
 | 
			
		||||
  border-color: transparent;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  color: #c2c6dc;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
@@ -198,25 +161,13 @@ export const TaskDetailsTitle = styled(TextareaAutosize)<{ loading?: boolean }>`
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  background: none;
 | 
			
		||||
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.loading
 | 
			
		||||
      ? css`
 | 
			
		||||
          background-image: linear-gradient(90deg, ${defaultBaseColor}, ${defaultHighlightColor}, ${defaultBaseColor});
 | 
			
		||||
          background-size: 200px 100%;
 | 
			
		||||
          background-repeat: no-repeat;
 | 
			
		||||
          animation: ${skeletonKeyframes} 1.2s ease-in-out infinite;
 | 
			
		||||
        `
 | 
			
		||||
      : css`
 | 
			
		||||
          &:hover {
 | 
			
		||||
            border-color: #414561;
 | 
			
		||||
            border-width: 1px;
 | 
			
		||||
            border-style: solid;
 | 
			
		||||
          }
 | 
			
		||||
  &:hover {
 | 
			
		||||
    border-color: #414561;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
          &:focus {
 | 
			
		||||
            border-color: ${props.theme.colors.primary};
 | 
			
		||||
          }
 | 
			
		||||
        `}
 | 
			
		||||
  &:focus {
 | 
			
		||||
    border-color: ${props => props.theme.colors.primary};
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const DueDateTitle = styled.div`
 | 
			
		||||
@@ -524,7 +475,6 @@ export const CommentEditorContainer = styled.div`
 | 
			
		||||
  border: 1px solid #414561;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  background: #1f243e;
 | 
			
		||||
`;
 | 
			
		||||
export const CommentProfile = styled(TaskAssignee)`
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
@@ -534,7 +484,7 @@ export const CommentProfile = styled(TaskAssignee)`
 | 
			
		||||
  align-items: normal;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: boolean }>`
 | 
			
		||||
export const CommentTextArea = styled(TextareaAutosize)`
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  line-height: 28px;
 | 
			
		||||
  padding: 4px 6px;
 | 
			
		||||
@@ -545,16 +495,14 @@ export const CommentTextArea = styled(TextareaAutosize)<{ showCommentActions: bo
 | 
			
		||||
  transition: max-height 200ms, height 200ms, min-height 200ms;
 | 
			
		||||
  min-height: 36px;
 | 
			
		||||
  max-height: 36px;
 | 
			
		||||
  ${props =>
 | 
			
		||||
    props.showCommentActions
 | 
			
		||||
      ? css`
 | 
			
		||||
          min-height: 80px;
 | 
			
		||||
          max-height: none;
 | 
			
		||||
          line-height: 20px;
 | 
			
		||||
        `
 | 
			
		||||
      : css`
 | 
			
		||||
          height: 36px;
 | 
			
		||||
        `}
 | 
			
		||||
  &:not(:focus) {
 | 
			
		||||
    height: 36px;
 | 
			
		||||
  }
 | 
			
		||||
  &:focus {
 | 
			
		||||
    min-height: 80px;
 | 
			
		||||
    max-height: none;
 | 
			
		||||
    line-height: 20px;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const CommentEditorActions = styled.div<{ visible: boolean }>`
 | 
			
		||||
@@ -581,18 +529,6 @@ export const ActivitySection = styled.div`
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
 | 
			
		||||
  padding: 8px 26px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column-reverse;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemCommentAction = styled.div`
 | 
			
		||||
  display: none;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  svg {
 | 
			
		||||
    fill: ${props => props.theme.colors.text.primary} !important;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItem = styled.div`
 | 
			
		||||
@@ -601,32 +537,25 @@ export const ActivityItem = styled.div`
 | 
			
		||||
  overflow-wrap: break-word;
 | 
			
		||||
  word-wrap: break-word;
 | 
			
		||||
  word-break: break-word;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  &:hover ${ActivityItemCommentAction} {
 | 
			
		||||
    display: flex;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemHeader = styled.div<{ editable?: boolean }>`
 | 
			
		||||
export const ActivityItemHeader = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  padding-left: 8px;
 | 
			
		||||
  ${props => props.editable && 'width: 100%;'}
 | 
			
		||||
`;
 | 
			
		||||
export const ActivityItemHeaderUser = styled(TaskAssignee)`
 | 
			
		||||
  align-items: start;
 | 
			
		||||
  margin-right: 4px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemHeaderTitle = styled.div`
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
  line-height: 18px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  padding-bottom: 2px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemHeaderTitleName = styled.span`
 | 
			
		||||
  color: ${props => props.theme.colors.text.primary};
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  padding-right: 3px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemTimestamp = styled.span<{ margin: number }>`
 | 
			
		||||
@@ -639,10 +568,8 @@ export const ActivityItemDetails = styled.div`
 | 
			
		||||
  margin-left: 32px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemCommentContainer = styled.div``;
 | 
			
		||||
export const ActivityItemComment = styled.div<{ editable: boolean }>`
 | 
			
		||||
export const ActivityItemComment = styled.div`
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  ${mixin.boxShadowCard}
 | 
			
		||||
  position: relative;
 | 
			
		||||
@@ -650,32 +577,6 @@ export const ActivityItemComment = styled.div<{ editable: boolean }>`
 | 
			
		||||
  padding: 8px 12px;
 | 
			
		||||
  margin: 4px 0;
 | 
			
		||||
  background-color: ${props => mixin.darken(props.theme.colors.alternate, 0.1)};
 | 
			
		||||
  ${props => props.editable && 'width: 100%;'}
 | 
			
		||||
 | 
			
		||||
  & span {
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
  & ul {
 | 
			
		||||
    list-style-type: disc;
 | 
			
		||||
    margin: 8px 0;
 | 
			
		||||
  }
 | 
			
		||||
  & ul > li {
 | 
			
		||||
    margin: 8px 8px 8px 24px;
 | 
			
		||||
    margin-inline-start: 24px;
 | 
			
		||||
    margin-inline-end: 8px;
 | 
			
		||||
  }
 | 
			
		||||
  & ul > li ul > li {
 | 
			
		||||
    list-style: circle;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemCommentActions = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 8px;
 | 
			
		||||
  right: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ActivityItemLog = styled.span`
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import Modal from 'shared/components/Modal';
 | 
			
		||||
import TaskDetails from '.';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: TaskDetails,
 | 
			
		||||
  title: 'TaskDetails',
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
      { name: 'gray', value: '#cdd3e1', default: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  const [description, setDescription] = useState('');
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <Modal
 | 
			
		||||
        width={1040}
 | 
			
		||||
        onClose={action('on close')}
 | 
			
		||||
        renderContent={() => {
 | 
			
		||||
          return (
 | 
			
		||||
            <TaskDetails
 | 
			
		||||
              onDeleteItem={action('delete item')}
 | 
			
		||||
              onChangeItemName={action('change item name')}
 | 
			
		||||
              task={{
 | 
			
		||||
                id: '1',
 | 
			
		||||
                taskGroup: { name: 'General', id: '1' },
 | 
			
		||||
                name: 'Hello, world',
 | 
			
		||||
                position: 1,
 | 
			
		||||
                labels: [
 | 
			
		||||
                  {
 | 
			
		||||
                    id: 'soft-skills',
 | 
			
		||||
                    assignedDate: new Date().toString(),
 | 
			
		||||
                    projectLabel: {
 | 
			
		||||
                      createdDate: new Date().toString(),
 | 
			
		||||
                      id: 'label-soft-skills',
 | 
			
		||||
                      name: 'Soft Skills',
 | 
			
		||||
                      labelColor: {
 | 
			
		||||
                        id: '1',
 | 
			
		||||
                        name: 'white',
 | 
			
		||||
                        colorHex: '#fff',
 | 
			
		||||
                        position: 1,
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
                description,
 | 
			
		||||
                assigned: [
 | 
			
		||||
                  {
 | 
			
		||||
                    id: '1',
 | 
			
		||||
                    profileIcon: { bgColor: null, url: null, initials: null },
 | 
			
		||||
                    fullName: 'Jordan Knott',
 | 
			
		||||
                  },
 | 
			
		||||
                ],
 | 
			
		||||
              }}
 | 
			
		||||
              onTaskNameChange={action('task name change')}
 | 
			
		||||
              onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
 | 
			
		||||
              onDeleteTask={action('delete task')}
 | 
			
		||||
              onCloseModal={action('close modal')}
 | 
			
		||||
              onMemberProfile={action('profile')}
 | 
			
		||||
              onOpenAddMemberPopup={action('open add member popup')}
 | 
			
		||||
              onAddItem={action('add item')}
 | 
			
		||||
              onToggleTaskComplete={action('toggle task complete')}
 | 
			
		||||
              onToggleChecklistItem={action('toggle checklist item')}
 | 
			
		||||
              onOpenAddLabelPopup={action('open add label popup')}
 | 
			
		||||
              onChangeChecklistName={action('change checklist name')}
 | 
			
		||||
              onDeleteChecklist={action('delete checklist')}
 | 
			
		||||
              onOpenAddChecklistPopup={action(' open checklist')}
 | 
			
		||||
              onOpenDueDatePopop={action('open due date popup')}
 | 
			
		||||
              onChecklistDrop={action('on checklist drop')}
 | 
			
		||||
              onChecklistItemDrop={action('on checklist item drop')}
 | 
			
		||||
            />
 | 
			
		||||
          );
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -12,31 +12,17 @@ import {
 | 
			
		||||
  At,
 | 
			
		||||
  Smile,
 | 
			
		||||
} from 'shared/icons';
 | 
			
		||||
import { toArray } from 'react-emoji-render';
 | 
			
		||||
import DOMPurify from 'dompurify';
 | 
			
		||||
import TaskAssignee from 'shared/components/TaskAssignee';
 | 
			
		||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
 | 
			
		||||
import { usePopup } from 'shared/components/PopupMenu';
 | 
			
		||||
import CommentCreator from 'shared/components/TaskDetails/CommentCreator';
 | 
			
		||||
import { AngleDown } from 'shared/icons/AngleDown';
 | 
			
		||||
import Editor from 'rich-markdown-editor';
 | 
			
		||||
import dark from 'shared/utils/editorTheme';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import ReactMarkdown from 'react-markdown';
 | 
			
		||||
import { Picker, Emoji } from 'emoji-mart';
 | 
			
		||||
import 'emoji-mart/css/emoji-mart.css';
 | 
			
		||||
 | 
			
		||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
 | 
			
		||||
import Task from 'shared/icons/Task';
 | 
			
		||||
import {
 | 
			
		||||
  ActivityItemHeader,
 | 
			
		||||
  ActivityItemTimestamp,
 | 
			
		||||
  ActivityItem,
 | 
			
		||||
  ActivityItemCommentAction,
 | 
			
		||||
  ActivityItemCommentActions,
 | 
			
		||||
  TaskDetailLabel,
 | 
			
		||||
  CommentContainer,
 | 
			
		||||
  ActivityItemCommentContainer,
 | 
			
		||||
  MetaDetailContent,
 | 
			
		||||
  TaskDetailsAddLabelIcon,
 | 
			
		||||
  ActionButton,
 | 
			
		||||
@@ -72,127 +58,18 @@ import {
 | 
			
		||||
  TaskMember,
 | 
			
		||||
  TabBarSection,
 | 
			
		||||
  TabBarItem,
 | 
			
		||||
  CommentTextArea,
 | 
			
		||||
  CommentEditorContainer,
 | 
			
		||||
  CommentEditorActions,
 | 
			
		||||
  CommentEditorActionIcon,
 | 
			
		||||
  CommentEditorSaveButton,
 | 
			
		||||
  CommentProfile,
 | 
			
		||||
  CommentInnerWrapper,
 | 
			
		||||
  ActivitySection,
 | 
			
		||||
  TaskDetailsEditor,
 | 
			
		||||
  ActivityItemHeaderUser,
 | 
			
		||||
  ActivityItemHeaderTitle,
 | 
			
		||||
  ActivityItemHeaderTitleName,
 | 
			
		||||
  ActivityItemComment,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
 | 
			
		||||
import onDragEnd from './onDragEnd';
 | 
			
		||||
import { plugin as em } from './remark';
 | 
			
		||||
import ActivityMessage from './ActivityMessage';
 | 
			
		||||
 | 
			
		||||
const parseEmojis = (value: string) => {
 | 
			
		||||
  const emojisArray = toArray(value);
 | 
			
		||||
 | 
			
		||||
  // toArray outputs React elements for emojis and strings for other
 | 
			
		||||
  const newValue = emojisArray.reduce((previous: any, current: any) => {
 | 
			
		||||
    if (typeof current === 'string') {
 | 
			
		||||
      return previous + current;
 | 
			
		||||
    }
 | 
			
		||||
    return previous + current.props.children;
 | 
			
		||||
  }, '');
 | 
			
		||||
 | 
			
		||||
  return newValue;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type StreamCommentProps = {
 | 
			
		||||
  comment?: TaskComment | null;
 | 
			
		||||
  onUpdateComment: (message: string) => void;
 | 
			
		||||
  onExtraActions: (commentID: string, $target: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onCancelCommentEdit: () => void;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
};
 | 
			
		||||
const StreamComment: React.FC<StreamCommentProps> = ({
 | 
			
		||||
  comment,
 | 
			
		||||
  onExtraActions,
 | 
			
		||||
  editable,
 | 
			
		||||
  onUpdateComment,
 | 
			
		||||
  onCancelCommentEdit,
 | 
			
		||||
}) => {
 | 
			
		||||
  const $actions = useRef<HTMLDivElement>(null);
 | 
			
		||||
  if (comment) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ActivityItem>
 | 
			
		||||
        <ActivityItemHeaderUser size={32} member={comment.createdBy} />
 | 
			
		||||
        <ActivityItemHeader editable={editable}>
 | 
			
		||||
          <ActivityItemHeaderTitle>
 | 
			
		||||
            <ActivityItemHeaderTitleName>{comment.createdBy.fullName}</ActivityItemHeaderTitleName>
 | 
			
		||||
            <ActivityItemTimestamp margin={8}>
 | 
			
		||||
              {dayjs(comment.createdAt).format('MMM D [at] h:mm A')}
 | 
			
		||||
              {comment.updatedAt && ' (edited)'}
 | 
			
		||||
            </ActivityItemTimestamp>
 | 
			
		||||
          </ActivityItemHeaderTitle>
 | 
			
		||||
          <ActivityItemCommentContainer>
 | 
			
		||||
            <ActivityItemComment editable={editable}>
 | 
			
		||||
              {editable ? (
 | 
			
		||||
                <CommentCreator
 | 
			
		||||
                  message={comment.message}
 | 
			
		||||
                  autoFocus
 | 
			
		||||
                  onCancelEdit={onCancelCommentEdit}
 | 
			
		||||
                  onCreateComment={onUpdateComment}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <ReactMarkdown escapeHtml={false} plugins={[em]}>
 | 
			
		||||
                  {DOMPurify.sanitize(comment.message, { FORBID_TAGS: ['style', 'img'] })}
 | 
			
		||||
                </ReactMarkdown>
 | 
			
		||||
              )}
 | 
			
		||||
            </ActivityItemComment>
 | 
			
		||||
            <ActivityItemCommentActions>
 | 
			
		||||
              <ActivityItemCommentAction
 | 
			
		||||
                ref={$actions}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  onExtraActions(comment.id, $actions);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <AngleDown width={18} height={18} />
 | 
			
		||||
              </ActivityItemCommentAction>
 | 
			
		||||
            </ActivityItemCommentActions>
 | 
			
		||||
          </ActivityItemCommentContainer>
 | 
			
		||||
        </ActivityItemHeader>
 | 
			
		||||
      </ActivityItem>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type StreamActivityProps = {
 | 
			
		||||
  activity?: TaskActivity | null;
 | 
			
		||||
};
 | 
			
		||||
const StreamActivity: React.FC<StreamActivityProps> = ({ activity }) => {
 | 
			
		||||
  if (activity) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ActivityItem>
 | 
			
		||||
        <ActivityItemHeaderUser
 | 
			
		||||
          size={32}
 | 
			
		||||
          member={{
 | 
			
		||||
            id: activity.causedBy.id,
 | 
			
		||||
            fullName: activity.causedBy.fullName,
 | 
			
		||||
            profileIcon: activity.causedBy.profileIcon
 | 
			
		||||
              ? activity.causedBy.profileIcon
 | 
			
		||||
              : {
 | 
			
		||||
                  url: null,
 | 
			
		||||
                  initials: activity.causedBy.fullName.charAt(0),
 | 
			
		||||
                  bgColor: '#fff',
 | 
			
		||||
                },
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <ActivityItemHeader>
 | 
			
		||||
          <ActivityItemHeaderTitle>
 | 
			
		||||
            <ActivityItemHeaderTitleName>{activity.causedBy.fullName}</ActivityItemHeaderTitleName>
 | 
			
		||||
            <ActivityMessage type={activity.type} data={activity.data} />
 | 
			
		||||
          </ActivityItemHeaderTitle>
 | 
			
		||||
          <ActivityItemTimestamp margin={0}>
 | 
			
		||||
            {dayjs(activity.createdAt).format('MMM D [at] h:mm A')}
 | 
			
		||||
          </ActivityItemTimestamp>
 | 
			
		||||
        </ActivityItemHeader>
 | 
			
		||||
      </ActivityItem>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ChecklistContainer = styled.div``;
 | 
			
		||||
 | 
			
		||||
@@ -237,13 +114,8 @@ type TaskDetailsProps = {
 | 
			
		||||
  onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onCreateComment: (task: Task, message: string) => void;
 | 
			
		||||
  onCommentShowActions: (commentID: string, $targetRef: React.RefObject<HTMLElement>) => void;
 | 
			
		||||
  onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
 | 
			
		||||
  onCancelCommentEdit: () => void;
 | 
			
		||||
  onUpdateComment: (commentID: string, message: string) => void;
 | 
			
		||||
  onChangeChecklistName: (checklistID: string, name: string) => void;
 | 
			
		||||
  editableComment?: string | null;
 | 
			
		||||
  onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
 | 
			
		||||
  onCloseModal: () => void;
 | 
			
		||||
  onChecklistDrop: (checklist: TaskChecklist) => void;
 | 
			
		||||
@@ -252,15 +124,11 @@ type TaskDetailsProps = {
 | 
			
		||||
 | 
			
		||||
const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
  me,
 | 
			
		||||
  onCancelCommentEdit,
 | 
			
		||||
  task,
 | 
			
		||||
  editableComment = null,
 | 
			
		||||
  onDeleteChecklist,
 | 
			
		||||
  onTaskNameChange,
 | 
			
		||||
  onCommentShowActions,
 | 
			
		||||
  onOpenAddChecklistPopup,
 | 
			
		||||
  onChangeChecklistName,
 | 
			
		||||
  onCreateComment,
 | 
			
		||||
  onChecklistDrop,
 | 
			
		||||
  onChecklistItemDrop,
 | 
			
		||||
  onToggleTaskComplete,
 | 
			
		||||
@@ -269,7 +137,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
  onDeleteItem,
 | 
			
		||||
  onDeleteTask,
 | 
			
		||||
  onCloseModal,
 | 
			
		||||
  onUpdateComment,
 | 
			
		||||
  onOpenAddMemberPopup,
 | 
			
		||||
  onOpenAddLabelPopup,
 | 
			
		||||
  onOpenDueDatePopop,
 | 
			
		||||
@@ -289,38 +156,11 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
  });
 | 
			
		||||
  const [saveTimeout, setSaveTimeout] = useState<any>(null);
 | 
			
		||||
  const [showRaw, setShowRaw] = useState(false);
 | 
			
		||||
  const [showCommentActions, setShowCommentActions] = useState(false);
 | 
			
		||||
  const taskDescriptionRef = useRef(task.description ?? '');
 | 
			
		||||
  const $noMemberBtn = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $addMemberBtn = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $dueDateBtn = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const $detailsTitle = useRef<HTMLTextAreaElement>(null);
 | 
			
		||||
 | 
			
		||||
  const activityStream: Array<{ id: string; data: { time: string; type: 'comment' | 'activity' } }> = [];
 | 
			
		||||
 | 
			
		||||
  if (task.activity) {
 | 
			
		||||
    task.activity.forEach(activity => {
 | 
			
		||||
      activityStream.push({
 | 
			
		||||
        id: activity.id,
 | 
			
		||||
        data: {
 | 
			
		||||
          time: activity.createdAt,
 | 
			
		||||
          type: 'activity',
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (task.comments) {
 | 
			
		||||
    task.comments.forEach(comment => {
 | 
			
		||||
      activityStream.push({
 | 
			
		||||
        id: comment.id,
 | 
			
		||||
        data: {
 | 
			
		||||
          time: comment.createdAt,
 | 
			
		||||
          type: 'comment',
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  activityStream.sort((a, b) => (dayjs(a.data.time).isAfter(dayjs(b.data.time)) ? 1 : -1));
 | 
			
		||||
 | 
			
		||||
  const saveDescription = () => {
 | 
			
		||||
    onTaskDescriptionChange(task, taskDescriptionRef.current);
 | 
			
		||||
@@ -342,9 +182,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {task.dueDate ? (
 | 
			
		||||
                <SidebarButtonText>
 | 
			
		||||
                  {dayjs(task.dueDate).format(task.hasTime ? 'MMM D [at] h:mm A' : 'MMMM D')}
 | 
			
		||||
                </SidebarButtonText>
 | 
			
		||||
                <SidebarButtonText>{dayjs(task.dueDate).format('MMM D [at] h:mm A')}</SidebarButtonText>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <SidebarButtonText>No due date</SidebarButtonText>
 | 
			
		||||
              )}
 | 
			
		||||
@@ -441,15 +279,6 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
          <TaskDetailsTitleWrapper>
 | 
			
		||||
            <TaskDetailsTitle
 | 
			
		||||
              value={taskName}
 | 
			
		||||
              ref={$detailsTitle}
 | 
			
		||||
              onKeyDown={e => {
 | 
			
		||||
                if (e.keyCode === 13) {
 | 
			
		||||
                  e.preventDefault();
 | 
			
		||||
                  if ($detailsTitle && $detailsTitle.current) {
 | 
			
		||||
                    $detailsTitle.current.blur();
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
              onChange={e => {
 | 
			
		||||
                setTaskName(e.currentTarget.value);
 | 
			
		||||
              }}
 | 
			
		||||
@@ -596,29 +425,46 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
 | 
			
		||||
          <TabBarSection>
 | 
			
		||||
            <TabBarItem>Activity</TabBarItem>
 | 
			
		||||
          </TabBarSection>
 | 
			
		||||
          <ActivitySection>
 | 
			
		||||
            {activityStream.map(stream =>
 | 
			
		||||
              stream.data.type === 'comment' ? (
 | 
			
		||||
                <StreamComment
 | 
			
		||||
                  onExtraActions={onCommentShowActions}
 | 
			
		||||
                  onCancelCommentEdit={onCancelCommentEdit}
 | 
			
		||||
                  onUpdateComment={message => onUpdateComment(stream.id, message)}
 | 
			
		||||
                  editable={stream.id === editableComment}
 | 
			
		||||
                  comment={task.comments && task.comments.find(comment => comment.id === stream.id)}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <StreamActivity activity={task.activity && task.activity.find(activity => activity.id === stream.id)} />
 | 
			
		||||
              ),
 | 
			
		||||
            )}
 | 
			
		||||
          </ActivitySection>
 | 
			
		||||
          <ActivitySection />
 | 
			
		||||
        </InnerContentContainer>
 | 
			
		||||
        <CommentContainer>
 | 
			
		||||
          {me && (
 | 
			
		||||
            <CommentCreator
 | 
			
		||||
              me={me}
 | 
			
		||||
              onCreateComment={message => onCreateComment(task, message)}
 | 
			
		||||
              onMemberProfile={onMemberProfile}
 | 
			
		||||
            />
 | 
			
		||||
            <CommentInnerWrapper>
 | 
			
		||||
              <CommentProfile
 | 
			
		||||
                member={me}
 | 
			
		||||
                size={32}
 | 
			
		||||
                onMemberProfile={$target => {
 | 
			
		||||
                  onMemberProfile($target, me.id);
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <CommentEditorContainer>
 | 
			
		||||
                <CommentTextArea
 | 
			
		||||
                  disabled
 | 
			
		||||
                  placeholder="Write a comment..."
 | 
			
		||||
                  onFocus={() => {
 | 
			
		||||
                    setShowCommentActions(true);
 | 
			
		||||
                  }}
 | 
			
		||||
                  onBlur={() => {
 | 
			
		||||
                    setShowCommentActions(false);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
                <CommentEditorActions visible={showCommentActions}>
 | 
			
		||||
                  <CommentEditorActionIcon>
 | 
			
		||||
                    <Paperclip width={12} height={12} />
 | 
			
		||||
                  </CommentEditorActionIcon>
 | 
			
		||||
                  <CommentEditorActionIcon>
 | 
			
		||||
                    <At width={12} height={12} />
 | 
			
		||||
                  </CommentEditorActionIcon>
 | 
			
		||||
                  <CommentEditorActionIcon>
 | 
			
		||||
                    <Smile width={12} height={12} />
 | 
			
		||||
                  </CommentEditorActionIcon>
 | 
			
		||||
                  <CommentEditorActionIcon>
 | 
			
		||||
                    <Task width={12} height={12} />
 | 
			
		||||
                  </CommentEditorActionIcon>
 | 
			
		||||
                  <CommentEditorSaveButton>Save</CommentEditorSaveButton>
 | 
			
		||||
                </CommentEditorActions>
 | 
			
		||||
              </CommentEditorContainer>
 | 
			
		||||
            </CommentInnerWrapper>
 | 
			
		||||
          )}
 | 
			
		||||
        </CommentContainer>
 | 
			
		||||
      </ContentContainer>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
import visit from 'unist-util-visit';
 | 
			
		||||
import emoji from 'node-emoji';
 | 
			
		||||
import emoticon from 'emoticon';
 | 
			
		||||
import { Emoji } from 'emoji-mart';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import ReactDOMServer from 'react-dom/server';
 | 
			
		||||
 | 
			
		||||
const RE_EMOJI = /:\+1:|:-1:|:[\w-]+:/g;
 | 
			
		||||
const RE_SHORT = /[$@|*'",;.=:\-)([\]\\/<>038BOopPsSdDxXzZ]{2,5}/g;
 | 
			
		||||
 | 
			
		||||
const DEFAULT_SETTINGS = {
 | 
			
		||||
  padSpaceAfter: false,
 | 
			
		||||
  emoticon: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function plugin(options) {
 | 
			
		||||
  const settings = Object.assign({}, DEFAULT_SETTINGS, options);
 | 
			
		||||
  const pad = !!settings.padSpaceAfter;
 | 
			
		||||
  const emoticonEnable = !!settings.emoticon;
 | 
			
		||||
 | 
			
		||||
  function getEmojiByShortCode(match) {
 | 
			
		||||
    // 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 iconPart = emoticon.find(e => e.emoticons.includes(match.slice(0, -1))); // second search pattern
 | 
			
		||||
    const trimmedChar = iconPart ? match.slice(-1) : '';
 | 
			
		||||
    const addPad = pad ? ' ' : '';
 | 
			
		||||
    let icon = iconFull ? iconFull.emoji + addPad : iconPart && iconPart.emoji + addPad + trimmedChar;
 | 
			
		||||
    return icon || match;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getEmoji(match) {
 | 
			
		||||
    console.log(match);
 | 
			
		||||
    const got = emoji.get(match);
 | 
			
		||||
    if (pad && got !== match) {
 | 
			
		||||
      return got + ' ';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(got);
 | 
			
		||||
    return ReactDOMServer.renderToStaticMarkup(<Emoji set="google" emoji={match} size={16} />);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function transformer(tree) {
 | 
			
		||||
    visit(tree, 'paragraph', function(node) {
 | 
			
		||||
      console.log(tree);
 | 
			
		||||
      // node.value = node.value.replace(RE_EMOJI, getEmoji);
 | 
			
		||||
      // jnode.type = 'html';
 | 
			
		||||
      // jnode.tagName = 'div';
 | 
			
		||||
      // jnode.value = '';
 | 
			
		||||
      for (let nodeIdx = 0; nodeIdx < node.children.length; nodeIdx++) {
 | 
			
		||||
        if (node.children[nodeIdx].type === 'text') {
 | 
			
		||||
          node.children[nodeIdx].type = 'html';
 | 
			
		||||
          node.children[nodeIdx].tagName = 'div';
 | 
			
		||||
          node.children[nodeIdx].value = node.children[nodeIdx].value.replace(RE_EMOJI, getEmoji);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (emoticonEnable) {
 | 
			
		||||
        // node.value = node.value.replace(RE_SHORT, getEmojiByShortCode);
 | 
			
		||||
      }
 | 
			
		||||
      console.log(node);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return transformer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { plugin };
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
import styled, { css } from 'styled-components';
 | 
			
		||||
import TextareaAutosize from 'react-autosize-textarea';
 | 
			
		||||
import { mixin } from 'shared/utils/styles';
 | 
			
		||||
import Button from 'shared/components/Button';
 | 
			
		||||
import { Taskcafe } from 'shared/icons';
 | 
			
		||||
import { NavLink, Link } from 'react-router-dom';
 | 
			
		||||
import TaskAssignee from 'shared/components/TaskAssignee';
 | 
			
		||||
import { useRef } from 'react';
 | 
			
		||||
 | 
			
		||||
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
 | 
			
		||||
  z-index: ${props => props.zIndex};
 | 
			
		||||
@@ -41,51 +43,12 @@ export const BreadcrumpSeparator = styled.span`
 | 
			
		||||
  font-size: 18px;
 | 
			
		||||
  margin: 0px 10px;
 | 
			
		||||
`;
 | 
			
		||||
export const ProjectInfo = styled.div`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ProjectSwitchInner = styled.div`
 | 
			
		||||
  border-radius: 12px;
 | 
			
		||||
  height: 48px;
 | 
			
		||||
  width: 48px;
 | 
			
		||||
  box-shadow: inset 0 -2px rgba(0, 0, 0, 0.05);
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  background: #cbd4db;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex: 0 0 auto;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  background-color: ${props => props.theme.colors.primary};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ProjectSwitch = styled.div`
 | 
			
		||||
  align-self: center;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin-right: 16px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  &::after {
 | 
			
		||||
    border-radius: 12px;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    content: '';
 | 
			
		||||
    left: 0;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
  }
 | 
			
		||||
  &:hover::after {
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.05);
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ProjectActions = styled.div`
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  align-items: flex-start;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  min-width: 1px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
@@ -104,11 +67,6 @@ export const ProfileNameWrapper = styled.div`
 | 
			
		||||
  line-height: 1.25;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const NavbarLink = styled(Link)`
 | 
			
		||||
  margin-right: 20px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const IconContainerWrapper = styled.div<{ disabled?: boolean }>`
 | 
			
		||||
  margin-right: 20px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
@@ -205,20 +163,7 @@ export const ProjectName = styled.h1`
 | 
			
		||||
  padding: 3px 10px 3px 8px;
 | 
			
		||||
  margin: -4px 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ProjectNameWrapper = styled.div`
 | 
			
		||||
  position: relative;
 | 
			
		||||
`;
 | 
			
		||||
export const ProjectNameSpan = styled.div`
 | 
			
		||||
  padding: 3px 10px 3px 8px;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  font-size: 20px;
 | 
			
		||||
`;
 | 
			
		||||
export const ProjectNameTextarea = styled.input`
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
export const ProjectNameTextarea = styled(TextareaAutosize)`
 | 
			
		||||
  border: none;
 | 
			
		||||
  resize: none;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
@@ -226,7 +171,7 @@ export const ProjectNameTextarea = styled.input`
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  margin: -4px 0;
 | 
			
		||||
 | 
			
		||||
  letter-spacing: normal;
 | 
			
		||||
  word-spacing: normal;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import NormalizeStyles from 'App/NormalizeStyles';
 | 
			
		||||
import BaseStyles from 'App/BaseStyles';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import TopNavbar from '.';
 | 
			
		||||
import theme from '../../../App/ThemeStyles';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  component: TopNavbar,
 | 
			
		||||
  title: 'TopNavbar',
 | 
			
		||||
 | 
			
		||||
  // Our exports that end in "Data" are not stories.
 | 
			
		||||
  excludeStories: /.*Data$/,
 | 
			
		||||
  parameters: {
 | 
			
		||||
    backgrounds: [
 | 
			
		||||
      { name: 'white', value: '#ffffff' },
 | 
			
		||||
      { name: 'gray', value: '#f8f8f8' },
 | 
			
		||||
      { name: 'darkBlue', value: theme.colors.bg.secondary, default: true },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Default = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NormalizeStyles />
 | 
			
		||||
      <BaseStyles />
 | 
			
		||||
      <TopNavbar
 | 
			
		||||
        onOpenProjectFinder={action('finder')}
 | 
			
		||||
        name="Projects"
 | 
			
		||||
        user={{
 | 
			
		||||
          id: '1',
 | 
			
		||||
          fullName: 'Jordan Knott',
 | 
			
		||||
          profileIcon: {
 | 
			
		||||
            url: null,
 | 
			
		||||
            initials: 'JK',
 | 
			
		||||
            bgColor: '#000',
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
        onChangeRole={action('change role')}
 | 
			
		||||
        onNotificationClick={action('notifications click')}
 | 
			
		||||
        onOpenSettings={action('open settings')}
 | 
			
		||||
        onDashboardClick={action('open dashboard')}
 | 
			
		||||
        onRemoveFromBoard={action('remove project')}
 | 
			
		||||
        onProfileClick={action('profile click')}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -7,17 +7,12 @@ import { RoleCode } from 'shared/generated/graphql';
 | 
			
		||||
import NOOP from 'shared/utils/noop';
 | 
			
		||||
import { useHistory } from 'react-router';
 | 
			
		||||
import {
 | 
			
		||||
  ProjectInfo,
 | 
			
		||||
  NavbarLink,
 | 
			
		||||
  TaskcafeLogo,
 | 
			
		||||
  TaskcafeTitle,
 | 
			
		||||
  ProjectFinder,
 | 
			
		||||
  LogoContainer,
 | 
			
		||||
  NavSeparator,
 | 
			
		||||
  IconContainerWrapper,
 | 
			
		||||
  ProjectSwitch,
 | 
			
		||||
  ProjectNameWrapper,
 | 
			
		||||
  ProjectNameSpan,
 | 
			
		||||
  ProjectNameTextarea,
 | 
			
		||||
  InviteButton,
 | 
			
		||||
  GlobalActions,
 | 
			
		||||
@@ -35,7 +30,6 @@ import {
 | 
			
		||||
  ProfileNameSecondary,
 | 
			
		||||
  ProjectMember,
 | 
			
		||||
  ProjectMembers,
 | 
			
		||||
  ProjectSwitchInner,
 | 
			
		||||
} from './Styles';
 | 
			
		||||
 | 
			
		||||
type IconContainerProps = {
 | 
			
		||||
@@ -79,7 +73,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
  const [isEditProjectName, setEditProjectName] = useState(false);
 | 
			
		||||
  const [projectName, setProjectName] = useState(initialProjectName);
 | 
			
		||||
  const $projectName = useRef<HTMLInputElement>(null);
 | 
			
		||||
  const $projectName = useRef<HTMLTextAreaElement>(null);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isEditProjectName && $projectName && $projectName.current) {
 | 
			
		||||
      $projectName.current.focus();
 | 
			
		||||
@@ -90,7 +84,7 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
 | 
			
		||||
    setProjectName(initialProjectName);
 | 
			
		||||
  }, [initialProjectName]);
 | 
			
		||||
 | 
			
		||||
  const onProjectNameChange = (event: React.FormEvent<HTMLInputElement>): void => {
 | 
			
		||||
  const onProjectNameChange = (event: React.FormEvent<HTMLTextAreaElement>): void => {
 | 
			
		||||
    setProjectName(event.currentTarget.value);
 | 
			
		||||
  };
 | 
			
		||||
  const onProjectNameBlur = () => {
 | 
			
		||||
@@ -112,17 +106,14 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {isEditProjectName ? (
 | 
			
		||||
        <ProjectNameWrapper>
 | 
			
		||||
          <ProjectNameSpan>{projectName}</ProjectNameSpan>
 | 
			
		||||
          <ProjectNameTextarea
 | 
			
		||||
            ref={$projectName}
 | 
			
		||||
            onChange={onProjectNameChange}
 | 
			
		||||
            onKeyDown={onProjectNameKeyDown}
 | 
			
		||||
            onBlur={onProjectNameBlur}
 | 
			
		||||
            spellCheck={false}
 | 
			
		||||
            value={projectName}
 | 
			
		||||
          />
 | 
			
		||||
        </ProjectNameWrapper>
 | 
			
		||||
        <ProjectNameTextarea
 | 
			
		||||
          ref={$projectName}
 | 
			
		||||
          onChange={onProjectNameChange}
 | 
			
		||||
          onKeyDown={onProjectNameKeyDown}
 | 
			
		||||
          onBlur={onProjectNameBlur}
 | 
			
		||||
          spellCheck={false}
 | 
			
		||||
          value={projectName}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <ProjectName
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
@@ -188,7 +179,6 @@ type NavBarProps = {
 | 
			
		||||
  onRemoveFromBoard?: (userID: string) => void;
 | 
			
		||||
  onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
 | 
			
		||||
  onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
 | 
			
		||||
  onMyTasksClick: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const NavBar: React.FC<NavBarProps> = ({
 | 
			
		||||
@@ -211,7 +201,6 @@ const NavBar: React.FC<NavBarProps> = ({
 | 
			
		||||
  onProfileClick,
 | 
			
		||||
  onNotificationClick,
 | 
			
		||||
  onDashboardClick,
 | 
			
		||||
  onMyTasksClick,
 | 
			
		||||
  user,
 | 
			
		||||
  projectMembers,
 | 
			
		||||
  onOpenSettings,
 | 
			
		||||
@@ -223,50 +212,43 @@ const NavBar: React.FC<NavBarProps> = ({
 | 
			
		||||
  };
 | 
			
		||||
  const history = useHistory();
 | 
			
		||||
  const { showPopup } = usePopup();
 | 
			
		||||
  const $finder = useRef<HTMLDivElement>(null);
 | 
			
		||||
  return (
 | 
			
		||||
    <NavbarWrapper>
 | 
			
		||||
      <NavbarHeader>
 | 
			
		||||
        <ProjectActions>
 | 
			
		||||
          <ProjectSwitch ref={$finder} onClick={e => onOpenProjectFinder($finder)}>
 | 
			
		||||
            <ProjectSwitchInner>
 | 
			
		||||
              <TaskcafeLogo innerColor="#9f46e4" outerColor="#000" width={32} height={32} />
 | 
			
		||||
            </ProjectSwitchInner>
 | 
			
		||||
          </ProjectSwitch>
 | 
			
		||||
          <ProjectInfo>
 | 
			
		||||
            <ProjectMeta>
 | 
			
		||||
              {name && (
 | 
			
		||||
                <ProjectHeading
 | 
			
		||||
                  onFavorite={onFavorite}
 | 
			
		||||
                  onOpenSettings={onOpenSettings}
 | 
			
		||||
                  name={name}
 | 
			
		||||
                  canEditProjectName={canEditProjectName}
 | 
			
		||||
                  onSaveProjectName={onSaveName}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </ProjectMeta>
 | 
			
		||||
          <ProjectMeta>
 | 
			
		||||
            {name && (
 | 
			
		||||
              <ProjectTabs>
 | 
			
		||||
                {menuType &&
 | 
			
		||||
                  menuType.map((menu, idx) => {
 | 
			
		||||
                    return (
 | 
			
		||||
                      <ProjectTab
 | 
			
		||||
                        key={menu.name}
 | 
			
		||||
                        to={menu.link}
 | 
			
		||||
                        exact
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                          // TODO
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        {menu.name}
 | 
			
		||||
                      </ProjectTab>
 | 
			
		||||
                    );
 | 
			
		||||
                  })}
 | 
			
		||||
              </ProjectTabs>
 | 
			
		||||
              <ProjectHeading
 | 
			
		||||
                onFavorite={onFavorite}
 | 
			
		||||
                onOpenSettings={onOpenSettings}
 | 
			
		||||
                name={name}
 | 
			
		||||
                canEditProjectName={canEditProjectName}
 | 
			
		||||
                onSaveProjectName={onSaveName}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </ProjectInfo>
 | 
			
		||||
          </ProjectMeta>
 | 
			
		||||
          {name && (
 | 
			
		||||
            <ProjectTabs>
 | 
			
		||||
              {menuType &&
 | 
			
		||||
                menuType.map((menu, idx) => {
 | 
			
		||||
                  return (
 | 
			
		||||
                    <ProjectTab
 | 
			
		||||
                      key={menu.name}
 | 
			
		||||
                      to={menu.link}
 | 
			
		||||
                      exact
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        // TODO
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {menu.name}
 | 
			
		||||
                    </ProjectTab>
 | 
			
		||||
                  );
 | 
			
		||||
                })}
 | 
			
		||||
            </ProjectTabs>
 | 
			
		||||
          )}
 | 
			
		||||
        </ProjectActions>
 | 
			
		||||
        <LogoContainer to="/">
 | 
			
		||||
          <TaskcafeLogo width={32} height={32} />
 | 
			
		||||
          <TaskcafeTitle>Taskcafé</TaskcafeTitle>
 | 
			
		||||
        </LogoContainer>
 | 
			
		||||
        <GlobalActions>
 | 
			
		||||
@@ -321,12 +303,12 @@ const NavBar: React.FC<NavBarProps> = ({
 | 
			
		||||
          <ProjectFinder onClick={onOpenProjectFinder} variant="gradient">
 | 
			
		||||
            Projects
 | 
			
		||||
          </ProjectFinder>
 | 
			
		||||
          <NavbarLink to="">
 | 
			
		||||
          <IconContainer onClick={() => onDashboardClick()}>
 | 
			
		||||
            <HomeDashboard width={20} height={20} />
 | 
			
		||||
          </NavbarLink>
 | 
			
		||||
          <NavbarLink to="/tasks">
 | 
			
		||||
          </IconContainer>
 | 
			
		||||
          <IconContainer disabled onClick={NOOP}>
 | 
			
		||||
            <CheckCircle width={20} height={20} />
 | 
			
		||||
          </NavbarLink>
 | 
			
		||||
          </IconContainer>
 | 
			
		||||
          <IconContainer disabled onClick={NOOP}>
 | 
			
		||||
            <ListUnordered width={20} height={20} />
 | 
			
		||||
          </IconContainer>
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -6,45 +6,10 @@ query findTask($taskID: UUID!) {
 | 
			
		||||
    dueDate
 | 
			
		||||
    position
 | 
			
		||||
    complete
 | 
			
		||||
    hasTime
 | 
			
		||||
    taskGroup {
 | 
			
		||||
      id
 | 
			
		||||
      name
 | 
			
		||||
    }
 | 
			
		||||
    comments {
 | 
			
		||||
      id
 | 
			
		||||
      pinned
 | 
			
		||||
      message
 | 
			
		||||
      createdAt
 | 
			
		||||
      updatedAt
 | 
			
		||||
      createdBy {
 | 
			
		||||
        id
 | 
			
		||||
        fullName
 | 
			
		||||
        profileIcon {
 | 
			
		||||
          initials
 | 
			
		||||
          bgColor
 | 
			
		||||
          url
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    activity {
 | 
			
		||||
      id
 | 
			
		||||
      type
 | 
			
		||||
      causedBy {
 | 
			
		||||
        id
 | 
			
		||||
        fullName
 | 
			
		||||
        profileIcon {
 | 
			
		||||
          initials
 | 
			
		||||
          bgColor
 | 
			
		||||
          url
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      createdAt
 | 
			
		||||
      data {
 | 
			
		||||
        name
 | 
			
		||||
        value
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    badges {
 | 
			
		||||
      checklist {
 | 
			
		||||
        total
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ const TASK_FRAGMENT = gql`
 | 
			
		||||
    name
 | 
			
		||||
    description
 | 
			
		||||
    dueDate
 | 
			
		||||
    hasTime
 | 
			
		||||
    complete
 | 
			
		||||
    completedAt
 | 
			
		||||
    position
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) {
 | 
			
		||||
  projects {
 | 
			
		||||
    id
 | 
			
		||||
    name
 | 
			
		||||
  }
 | 
			
		||||
  myTasks(input: { status: $status, sort: $sort }) {
 | 
			
		||||
    tasks {
 | 
			
		||||
      id
 | 
			
		||||
      taskGroup {
 | 
			
		||||
        id
 | 
			
		||||
        name
 | 
			
		||||
      }
 | 
			
		||||
      name
 | 
			
		||||
      dueDate
 | 
			
		||||
      hasTime
 | 
			
		||||
      complete
 | 
			
		||||
      completedAt
 | 
			
		||||
    }
 | 
			
		||||
    projects {
 | 
			
		||||
      projectID
 | 
			
		||||
      taskID
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,8 +2,8 @@ import gql from 'graphql-tag';
 | 
			
		||||
import TASK_FRAGMENT from '../fragments/task';
 | 
			
		||||
 | 
			
		||||
const CREATE_TASK_MUTATION = gql`
 | 
			
		||||
  mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!, $assigned: [UUID!]) {
 | 
			
		||||
    createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position, assigned: $assigned }) {
 | 
			
		||||
  mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!) {
 | 
			
		||||
    createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
 | 
			
		||||
      ...TaskFields
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
import gql from 'graphql-tag';
 | 
			
		||||
 | 
			
		||||
const CREATE_TASK_MUTATION = gql`
 | 
			
		||||
  mutation createTaskComment($taskID: UUID!, $message: String!) {
 | 
			
		||||
    createTaskComment(input: { taskID: $taskID, message: $message }) {
 | 
			
		||||
      taskID
 | 
			
		||||
      comment {
 | 
			
		||||
        id
 | 
			
		||||
        message
 | 
			
		||||
        pinned
 | 
			
		||||
        createdAt
 | 
			
		||||
        updatedAt
 | 
			
		||||
        createdBy {
 | 
			
		||||
          id
 | 
			
		||||
          fullName
 | 
			
		||||
          profileIcon {
 | 
			
		||||
            initials
 | 
			
		||||
            bgColor
 | 
			
		||||
            url
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default CREATE_TASK_MUTATION;
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
import gql from 'graphql-tag';
 | 
			
		||||
 | 
			
		||||
const CREATE_TASK_MUTATION = gql`
 | 
			
		||||
  mutation deleteTaskComment($commentID: UUID!) {
 | 
			
		||||
    deleteTaskComment(input: { commentID: $commentID }) {
 | 
			
		||||
      commentID
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default CREATE_TASK_MUTATION;
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import gql from 'graphql-tag';
 | 
			
		||||
 | 
			
		||||
const CREATE_TASK_MUTATION = gql`
 | 
			
		||||
  mutation updateTaskComment($commentID: UUID!, $message: String!) {
 | 
			
		||||
    updateTaskComment(input: { commentID: $commentID, message: $message }) {
 | 
			
		||||
      comment {
 | 
			
		||||
        id
 | 
			
		||||
        updatedAt
 | 
			
		||||
        message
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default CREATE_TASK_MUTATION;
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!) {
 | 
			
		||||
mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time) {
 | 
			
		||||
  updateTaskDueDate (
 | 
			
		||||
    input: {
 | 
			
		||||
      taskID: $taskID
 | 
			
		||||
      dueDate: $dueDate
 | 
			
		||||
      hasTime: $hasTime
 | 
			
		||||
    }
 | 
			
		||||
  ) {
 | 
			
		||||
    id
 | 
			
		||||
    dueDate
 | 
			
		||||
    hasTime
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,14 +16,10 @@ const useOnOutsideClick = (
 | 
			
		||||
 | 
			
		||||
    const handleMouseUp = (event: any) => {
 | 
			
		||||
      if (typeof $ignoredElementRefsMemoized !== 'undefined') {
 | 
			
		||||
        const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(($elementRef: any) => {
 | 
			
		||||
          if ($elementRef && $elementRef.current) {
 | 
			
		||||
            return (
 | 
			
		||||
              $elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target)
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
          return false;
 | 
			
		||||
        });
 | 
			
		||||
        const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(
 | 
			
		||||
          ($elementRef: any) =>
 | 
			
		||||
            $elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target),
 | 
			
		||||
        );
 | 
			
		||||
        if (event.button === 0 && !isAnyIgnoredElementAncestorOfTarget) {
 | 
			
		||||
          onOutsideClick();
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
function useStickyState<T>(defaultValue: any, key: string): [T, React.Dispatch<React.SetStateAction<T>>] {
 | 
			
		||||
  const [value, setValue] = React.useState<T>(() => {
 | 
			
		||||
    const stickyValue = window.localStorage.getItem(key);
 | 
			
		||||
    return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
 | 
			
		||||
  });
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    window.localStorage.setItem(key, JSON.stringify(value));
 | 
			
		||||
  }, [key, value]);
 | 
			
		||||
  return [value, setValue];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useStickyState;
 | 
			
		||||
@@ -1,21 +1,12 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
export const AngleDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 512 512">
 | 
			
		||||
      <path d="M143 352.3L7 216.3c-9.4-9.4-9.4-24.6 0-33.9l22.6-22.6c9.4-9.4 24.6-9.4 33.9 0l96.4 96.4 96.4-96.4c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9l-136 136c-9.2 9.4-24.4 9.4-33.8 0z" />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  width?: number | string;
 | 
			
		||||
  height?: number | string;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  width: number | string;
 | 
			
		||||
  height: number | string;
 | 
			
		||||
  color: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const AngleDownOld = ({ width, height, color }: Props) => {
 | 
			
		||||
const AngleDown = ({ width, height, color }: Props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
 | 
			
		||||
      <path
 | 
			
		||||
@@ -26,10 +17,10 @@ const AngleDownOld = ({ width, height, color }: Props) => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AngleDownOld.defaultProps = {
 | 
			
		||||
AngleDown.defaultProps = {
 | 
			
		||||
  width: 24,
 | 
			
		||||
  height: 16,
 | 
			
		||||
  color: '#000',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AngleDownOld;
 | 
			
		||||
export default AngleDown;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
const Briefcase: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 512 512">
 | 
			
		||||
      <path d="M320 336c0 8.84-7.16 16-16 16h-96c-8.84 0-16-7.16-16-16v-48H0v144c0 25.6 22.4 48 48 48h416c25.6 0 48-22.4 48-48V288H320v48zm144-208h-80V80c0-25.6-22.4-48-48-48H176c-25.6 0-48 22.4-48 48v48H48c-25.6 0-48 22.4-48 48v80h512v-80c0-25.6-22.4-48-48-48zm-144 0H192V96h128v32z" />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Briefcase;
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
const CheckCircleOutline: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 512 512">
 | 
			
		||||
      <path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z" />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default CheckCircleOutline;
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
const ChevronRight: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 512 512">
 | 
			
		||||
      <path d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z" />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ChevronRight;
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
const Cogs: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 640 512">
 | 
			
		||||
      <path d="M512.1 191l-8.2 14.3c-3 5.3-9.4 7.5-15.1 5.4-11.8-4.4-22.6-10.7-32.1-18.6-4.6-3.8-5.8-10.5-2.8-15.7l8.2-14.3c-6.9-8-12.3-17.3-15.9-27.4h-16.5c-6 0-11.2-4.3-12.2-10.3-2-12-2.1-24.6 0-37.1 1-6 6.2-10.4 12.2-10.4h16.5c3.6-10.1 9-19.4 15.9-27.4l-8.2-14.3c-3-5.2-1.9-11.9 2.8-15.7 9.5-7.9 20.4-14.2 32.1-18.6 5.7-2.1 12.1.1 15.1 5.4l8.2 14.3c10.5-1.9 21.2-1.9 31.7 0L552 6.3c3-5.3 9.4-7.5 15.1-5.4 11.8 4.4 22.6 10.7 32.1 18.6 4.6 3.8 5.8 10.5 2.8 15.7l-8.2 14.3c6.9 8 12.3 17.3 15.9 27.4h16.5c6 0 11.2 4.3 12.2 10.3 2 12 2.1 24.6 0 37.1-1 6-6.2 10.4-12.2 10.4h-16.5c-3.6 10.1-9 19.4-15.9 27.4l8.2 14.3c3 5.2 1.9 11.9-2.8 15.7-9.5 7.9-20.4 14.2-32.1 18.6-5.7 2.1-12.1-.1-15.1-5.4l-8.2-14.3c-10.4 1.9-21.2 1.9-31.7 0zm-10.5-58.8c38.5 29.6 82.4-14.3 52.8-52.8-38.5-29.7-82.4 14.3-52.8 52.8zM386.3 286.1l33.7 16.8c10.1 5.8 14.5 18.1 10.5 29.1-8.9 24.2-26.4 46.4-42.6 65.8-7.4 8.9-20.2 11.1-30.3 5.3l-29.1-16.8c-16 13.7-34.6 24.6-54.9 31.7v33.6c0 11.6-8.3 21.6-19.7 23.6-24.6 4.2-50.4 4.4-75.9 0-11.5-2-20-11.9-20-23.6V418c-20.3-7.2-38.9-18-54.9-31.7L74 403c-10 5.8-22.9 3.6-30.3-5.3-16.2-19.4-33.3-41.6-42.2-65.7-4-10.9.4-23.2 10.5-29.1l33.3-16.8c-3.9-20.9-3.9-42.4 0-63.4L12 205.8c-10.1-5.8-14.6-18.1-10.5-29 8.9-24.2 26-46.4 42.2-65.8 7.4-8.9 20.2-11.1 30.3-5.3l29.1 16.8c16-13.7 34.6-24.6 54.9-31.7V57.1c0-11.5 8.2-21.5 19.6-23.5 24.6-4.2 50.5-4.4 76-.1 11.5 2 20 11.9 20 23.6v33.6c20.3 7.2 38.9 18 54.9 31.7l29.1-16.8c10-5.8 22.9-3.6 30.3 5.3 16.2 19.4 33.2 41.6 42.1 65.8 4 10.9.1 23.2-10 29.1l-33.7 16.8c3.9 21 3.9 42.5 0 63.5zm-117.6 21.1c59.2-77-28.7-164.9-105.7-105.7-59.2 77 28.7 164.9 105.7 105.7zm243.4 182.7l-8.2 14.3c-3 5.3-9.4 7.5-15.1 5.4-11.8-4.4-22.6-10.7-32.1-18.6-4.6-3.8-5.8-10.5-2.8-15.7l8.2-14.3c-6.9-8-12.3-17.3-15.9-27.4h-16.5c-6 0-11.2-4.3-12.2-10.3-2-12-2.1-24.6 0-37.1 1-6 6.2-10.4 12.2-10.4h16.5c3.6-10.1 9-19.4 15.9-27.4l-8.2-14.3c-3-5.2-1.9-11.9 2.8-15.7 9.5-7.9 20.4-14.2 32.1-18.6 5.7-2.1 12.1.1 15.1 5.4l8.2 14.3c10.5-1.9 21.2-1.9 31.7 0l8.2-14.3c3-5.3 9.4-7.5 15.1-5.4 11.8 4.4 22.6 10.7 32.1 18.6 4.6 3.8 5.8 10.5 2.8 15.7l-8.2 14.3c6.9 8 12.3 17.3 15.9 27.4h16.5c6 0 11.2 4.3 12.2 10.3 2 12 2.1 24.6 0 37.1-1 6-6.2 10.4-12.2 10.4h-16.5c-3.6 10.1-9 19.4-15.9 27.4l8.2 14.3c3 5.2 1.9 11.9-2.8 15.7-9.5 7.9-20.4 14.2-32.1 18.6-5.7 2.1-12.1-.1-15.1-5.4l-8.2-14.3c-10.4 1.9-21.2 1.9-31.7 0zM501.6 431c38.5 29.6 82.4-14.3 52.8-52.8-38.5-29.6-82.4 14.3-52.8 52.8z" />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Cogs;
 | 
			
		||||
@@ -1,29 +1,18 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Icon, { IconProps } from './Icon';
 | 
			
		||||
 | 
			
		||||
type TaskcafeProps = {
 | 
			
		||||
  innerColor?: string;
 | 
			
		||||
  outerColor?: string;
 | 
			
		||||
} & IconProps;
 | 
			
		||||
 | 
			
		||||
const Taskcafe: React.FC<TaskcafeProps> = ({
 | 
			
		||||
  innerColor = '#262c49',
 | 
			
		||||
  outerColor = '#7367f0',
 | 
			
		||||
  width = '16px',
 | 
			
		||||
  height = '16px',
 | 
			
		||||
  className,
 | 
			
		||||
}) => {
 | 
			
		||||
const Taskcafe: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Icon width={width} height={height} className={className} viewBox="0 0 800 800">
 | 
			
		||||
      <path
 | 
			
		||||
        d="M371.147 371.04c-59.092 0-106.995 47.903-106.995 106.995 0 59.092 47.903 106.995 106.995 106.995 59.092 0 106.995-47.903 106.995-106.995 0-59.092-47.903-106.996-106.995-106.996zm0 20.708c47.687 0 86.287 38.592 86.287 86.287 0 47.687-38.592 86.286-86.287 86.286-47.687 0-86.286-38.592-86.286-86.286 0-47.688 38.592-86.287 86.286-86.287m60.489 56.201l-9.723-9.8a5.177 5.177 0 00-7.321-.03l-60.984 60.494-25.796-26.006a5.177 5.177 0 00-7.322-.03l-9.802 9.723a5.177 5.177 0 00-.029 7.322l39.166 39.483a5.177 5.177 0 007.321.03l74.46-73.864a5.178 5.178 0 00.03-7.321z"
 | 
			
		||||
        fill={outerColor}
 | 
			
		||||
        fill="#7367f0"
 | 
			
		||||
      />
 | 
			
		||||
      <path
 | 
			
		||||
        d="M264.69 682.877H467.7c56.038 0 101.504-45.465 101.504-101.504h33.835c74.648 0 135.34-60.692 135.34-135.34s-60.692-135.34-135.34-135.34H188.562a25.315 25.315 0 00-25.376 25.377v245.303c0 56.039 45.465 101.504 101.504 101.504zM603.04 378.364c37.324 0 67.67 30.345 67.67 67.67 0 37.323-30.346 67.669-67.67 67.669h-33.835v-135.34zm50.435 406.018H112.75c-50.329 0-64.497-67.67-38.064-67.67h616.746c26.434 0 12.477 67.67-37.958 67.67z"
 | 
			
		||||
        fill={outerColor}
 | 
			
		||||
        fill="#7367f0"
 | 
			
		||||
      />
 | 
			
		||||
      <g fill="none" stroke={outerColor} strokeWidth="3.392">
 | 
			
		||||
      <g fill="none" stroke="#7367f0" strokeWidth="3.392">
 | 
			
		||||
        <path
 | 
			
		||||
          d="M286.302 276.2c-12.491-12.65-24.51-24.821-23.906-35.239.604-10.417 14.068-18.838 26.772-30.358 12.705-11.52 24.65-26.139 14.907-39.935-9.743-13.797-41.175-26.772-40.697-42.89.478-16.119 32.868-35.378 37.236-52.814 4.369-17.435-19.286-33.048-42.944-48.662M374.624 276.2c-12.492-12.65-24.51-24.821-23.907-35.239.604-10.417 14.068-18.838 26.773-30.358 12.704-11.52 24.65-26.139 14.906-39.935-9.742-13.797-41.174-26.772-40.696-42.89.478-16.119 32.867-35.378 37.236-52.814 4.368-17.435-19.287-33.048-42.944-48.662M462.945 276.2c-12.491-12.65-24.51-24.821-23.906-35.239.604-10.417 14.068-18.838 26.772-30.358 12.705-11.52 24.65-26.139 14.907-39.935-9.743-13.797-41.175-26.772-40.697-42.89.478-16.119 32.868-35.378 37.236-52.814 4.369-17.436-19.286-33.048-42.944-48.662"
 | 
			
		||||
          strokeWidth="28.00374144"
 | 
			
		||||
@@ -31,7 +20,7 @@ const Taskcafe: React.FC<TaskcafeProps> = ({
 | 
			
		||||
      </g>
 | 
			
		||||
      <path
 | 
			
		||||
        d="M371.147 363.194c-73.749 0-133.534 59.785-133.534 133.534 0 73.75 59.785 133.535 133.534 133.535 73.75 0 133.535-59.786 133.535-133.535s-59.786-133.534-133.535-133.534zm0 25.845c59.516 0 107.69 48.165 107.69 107.69 0 59.515-48.165 107.688-107.69 107.688-59.515 0-107.689-48.164-107.689-107.689 0-59.515 48.165-107.689 107.69-107.689m75.492 70.142l-12.135-12.232a6.462 6.462 0 00-9.138-.039l-76.11 75.498-32.194-32.456a6.463 6.463 0 00-9.138-.038l-12.233 12.134a6.451 6.451 0 00-.025 9.138l48.88 49.277a6.462 6.462 0 009.138.038l92.93-92.183a6.452 6.452 0 00.024-9.138z"
 | 
			
		||||
        fill={innerColor}
 | 
			
		||||
        fill="#262c49"
 | 
			
		||||
      />
 | 
			
		||||
    </Icon>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
import Cross from './Cross';
 | 
			
		||||
import Cog from './Cog';
 | 
			
		||||
import Cogs from './Cogs';
 | 
			
		||||
import ArrowDown from './ArrowDown';
 | 
			
		||||
import CheckCircleOutline from './CheckCircleOutline';
 | 
			
		||||
import Briefcase from './Briefcase';
 | 
			
		||||
import ListUnordered from './ListUnordered';
 | 
			
		||||
import ChevronRight from './ChevronRight';
 | 
			
		||||
import Dot from './Dot';
 | 
			
		||||
import CaretDown from './CaretDown';
 | 
			
		||||
import Eye from './Eye';
 | 
			
		||||
@@ -106,9 +102,5 @@ export {
 | 
			
		||||
  Dot,
 | 
			
		||||
  ArrowDown,
 | 
			
		||||
  CaretRight,
 | 
			
		||||
  CheckCircleOutline,
 | 
			
		||||
  Briefcase,
 | 
			
		||||
  DotCircle,
 | 
			
		||||
  ChevronRight,
 | 
			
		||||
  Cogs,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ export function updateApolloCache<T>(
 | 
			
		||||
  update: UpdateCacheFn<T>,
 | 
			
		||||
  variables?: object,
 | 
			
		||||
) {
 | 
			
		||||
  let queryArgs: DataProxy.Query<any, any>;
 | 
			
		||||
  let queryArgs: DataProxy.Query<any>;
 | 
			
		||||
  if (variables) {
 | 
			
		||||
    queryArgs = {
 | 
			
		||||
      query: document,
 | 
			
		||||
 
 | 
			
		||||
@@ -61,9 +61,9 @@ export const base = {
 | 
			
		||||
export const dark = {
 | 
			
		||||
  ...base,
 | 
			
		||||
  background: 'transparent',
 | 
			
		||||
  text: `${theme.colors.text.primary}`,
 | 
			
		||||
  code: `${theme.colors.text.primary}`,
 | 
			
		||||
  cursor: `${theme.colors.text.primary}`,
 | 
			
		||||
  text: `rgba(${theme.colors.text.primary})`,
 | 
			
		||||
  code: `rgba(${theme.colors.text.primary})`,
 | 
			
		||||
  cursor: `rgba(${theme.colors.text.primary})`,
 | 
			
		||||
  divider: '#4E5C6E',
 | 
			
		||||
  placeholder: '#52657A',
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								frontend/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										50
									
								
								frontend/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,10 +1,3 @@
 | 
			
		||||
type ProjectLabel = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  createdDate: string;
 | 
			
		||||
  name?: string | null;
 | 
			
		||||
  labelColor: LabelColor;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ProfileIcon = {
 | 
			
		||||
  url?: string | null;
 | 
			
		||||
  initials?: string | null;
 | 
			
		||||
@@ -63,46 +56,12 @@ type TaskBadges = {
 | 
			
		||||
  checklist?: ChecklistBadge | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type TaskActivityData = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type CausedBy = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  fullName: string;
 | 
			
		||||
  profileIcon?: null | ProfileIcon;
 | 
			
		||||
};
 | 
			
		||||
type TaskActivity = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  type: any;
 | 
			
		||||
  data: Array<TaskActivityData>;
 | 
			
		||||
  causedBy: CausedBy;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type CreatedBy = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  fullName: string;
 | 
			
		||||
  profileIcon: ProfileIcon;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type TaskComment = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  createdBy: CreatedBy;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
  updatedAt?: string | null;
 | 
			
		||||
  pinned: boolean;
 | 
			
		||||
  message: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Task = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  taskGroup: InnerTaskGroup;
 | 
			
		||||
  name: string;
 | 
			
		||||
  badges?: TaskBadges;
 | 
			
		||||
  position: number;
 | 
			
		||||
  hasTime?: boolean;
 | 
			
		||||
  dueDate?: string;
 | 
			
		||||
  complete?: boolean;
 | 
			
		||||
  completedAt?: string | null;
 | 
			
		||||
@@ -110,8 +69,6 @@ type Task = {
 | 
			
		||||
  description?: string | null;
 | 
			
		||||
  assigned?: Array<TaskUser>;
 | 
			
		||||
  checklists?: Array<TaskChecklist> | null;
 | 
			
		||||
  activity?: Array<TaskActivity> | null;
 | 
			
		||||
  comments?: Array<TaskComment> | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Project = {
 | 
			
		||||
@@ -132,3 +89,10 @@ type Team = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ProjectLabel = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  createdDate: string;
 | 
			
		||||
  name?: string | null;
 | 
			
		||||
  labelColor: LabelColor;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10039
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										10039
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										6
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.mod
									
									
									
									
									
								
							@@ -5,17 +5,14 @@ go 1.13
 | 
			
		||||
require (
 | 
			
		||||
	github.com/99designs/gqlgen v0.11.3
 | 
			
		||||
	github.com/RichardKnop/machinery v1.9.1
 | 
			
		||||
	github.com/brianvoe/gofakeit/v5 v5.11.2
 | 
			
		||||
	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 | 
			
		||||
	github.com/go-chi/chi v3.3.2+incompatible
 | 
			
		||||
	github.com/golang-migrate/migrate/v4 v4.11.0
 | 
			
		||||
	github.com/google/uuid v1.1.1
 | 
			
		||||
	github.com/jinzhu/now v1.1.1
 | 
			
		||||
	github.com/jmoiron/sqlx v1.2.0
 | 
			
		||||
	github.com/lib/pq v1.3.0
 | 
			
		||||
	github.com/lithammer/fuzzysearch v1.1.0
 | 
			
		||||
	github.com/magefile/mage v1.11.0
 | 
			
		||||
	github.com/manifoldco/promptui v0.8.0
 | 
			
		||||
	github.com/magefile/mage v1.9.0
 | 
			
		||||
	github.com/matcornic/hermes/v2 v2.1.0
 | 
			
		||||
	github.com/pelletier/go-toml v1.8.0 // indirect
 | 
			
		||||
	github.com/pkg/errors v0.9.1
 | 
			
		||||
@@ -26,6 +23,5 @@ require (
 | 
			
		||||
	github.com/spf13/viper v1.4.0
 | 
			
		||||
	github.com/vektah/gqlparser/v2 v2.0.1
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
 | 
			
		||||
	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 | 
			
		||||
	gopkg.in/mail.v2 v2.3.1
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								go.sum
									
									
									
									
									
								
							@@ -96,19 +96,11 @@ github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwj
 | 
			
		||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 | 
			
		||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
 | 
			
		||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
 | 
			
		||||
github.com/brianvoe/gofakeit v1.2.0 h1:GGbzCqQx9ync4ObAUhRa3F/M73eL9VZL3X09WoTwphM=
 | 
			
		||||
github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8=
 | 
			
		||||
github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc=
 | 
			
		||||
github.com/brianvoe/gofakeit/v5 v5.11.2 h1:Ny5Nsf4z2023ZvYP8ujW8p5B1t5sxhdFaQ/0IYXbeSA=
 | 
			
		||||
github.com/brianvoe/gofakeit/v5 v5.11.2/go.mod h1:/ZENnKqX+XrN8SORLe/fu5lZDIo1tuPncWuRD+eyhSI=
 | 
			
		||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 | 
			
		||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 | 
			
		||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 | 
			
		||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
 | 
			
		||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 | 
			
		||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
 | 
			
		||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 | 
			
		||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
 | 
			
		||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 | 
			
		||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
			
		||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
 | 
			
		||||
@@ -346,8 +338,6 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
 | 
			
		||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 | 
			
		||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
 | 
			
		||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 | 
			
		||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
 | 
			
		||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
 | 
			
		||||
@@ -359,8 +349,6 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22
 | 
			
		||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 | 
			
		||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
 | 
			
		||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 | 
			
		||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
 | 
			
		||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
 | 
			
		||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 | 
			
		||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
 | 
			
		||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
 | 
			
		||||
@@ -393,30 +381,23 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 | 
			
		||||
github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A=
 | 
			
		||||
github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ=
 | 
			
		||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 | 
			
		||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
 | 
			
		||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
 | 
			
		||||
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
 | 
			
		||||
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
 | 
			
		||||
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
 | 
			
		||||
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
 | 
			
		||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
 | 
			
		||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 | 
			
		||||
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
 | 
			
		||||
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
 | 
			
		||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
 | 
			
		||||
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
 | 
			
		||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
 | 
			
		||||
github.com/matcornic/hermes v1.2.0 h1:AuqZpYcTOtTB7cahdevLfnhIpfzmpqw5Czv8vpdnFDU=
 | 
			
		||||
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
 | 
			
		||||
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
 | 
			
		||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
 | 
			
		||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
 | 
			
		||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 | 
			
		||||
@@ -911,8 +892,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 | 
			
		||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 | 
			
		||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 | 
			
		||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 | 
			
		||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 | 
			
		||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 | 
			
		||||
 
 | 
			
		||||
@@ -52,12 +52,12 @@ func (r *ErrMalformedToken) Error() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAccessToken generates a new JWT access token with the correct claims
 | 
			
		||||
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string, jwtKey []byte, expirationTime time.Duration) (string, error) {
 | 
			
		||||
func NewAccessToken(userID string, restrictedMode RestrictedMode, orgRole string, jwtKey []byte) (string, error) {
 | 
			
		||||
	role := RoleMember
 | 
			
		||||
	if orgRole == "admin" {
 | 
			
		||||
		role = RoleAdmin
 | 
			
		||||
	}
 | 
			
		||||
	accessExpirationTime := time.Now().Add(expirationTime)
 | 
			
		||||
	accessExpirationTime := time.Now().Add(5 * time.Second)
 | 
			
		||||
	accessClaims := &AccessTokenClaims{
 | 
			
		||||
		UserID:         userID,
 | 
			
		||||
		Restricted:     restrictedMode,
 | 
			
		||||
@@ -98,6 +98,10 @@ func ValidateAccessToken(accessTokenString string, jwtKey []byte) (AccessTokenCl
 | 
			
		||||
		return jwtKey, nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return *accessClaims, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if accessToken.Valid {
 | 
			
		||||
		log.WithFields(log.Fields{
 | 
			
		||||
			"token":        accessTokenString,
 | 
			
		||||
@@ -107,7 +111,7 @@ func ValidateAccessToken(accessTokenString string, jwtKey []byte) (AccessTokenCl
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ve, ok := err.(*jwt.ValidationError); ok {
 | 
			
		||||
		if ve.Errors&(jwt.ValidationErrorMalformed|jwt.ValidationErrorSignatureInvalid) != 0 {
 | 
			
		||||
		if ve.Errors&jwt.ValidationErrorMalformed != 0 {
 | 
			
		||||
			return AccessTokenClaims{}, &ErrMalformedToken{}
 | 
			
		||||
		} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
 | 
			
		||||
			return AccessTokenClaims{}, &ErrExpiredToken{}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user