├── .github └── workflows │ ├── codeql-analysis.yml │ ├── gh-pages.yml │ └── node.js.yml ├── .gitignore ├── .gitpod.yml ├── LICENSE ├── README.md ├── demo-rig.gif ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── State ├── Reducer.js └── Reducer.test.js ├── components ├── Gantt │ ├── Gantt.css │ ├── Gantt.js │ ├── GanttAttachEvent.js │ ├── GanttConfig.js │ ├── GanttTemplates.js │ └── index.js ├── PageHeader │ ├── PageHeader.css │ ├── PageHeader.js │ └── index.js └── Toolbar │ ├── Toolbar.css │ ├── Toolbar.js │ └── index.js ├── functions ├── Common │ ├── CommonHelper.js │ ├── CommonHelper.test.js │ ├── IssueAPI.js │ ├── IssueAPI.test.js │ ├── Parser.js │ └── Parser.test.js ├── GitHub │ ├── GitHubAPI.js │ ├── GitHubHelper.js │ ├── GitHubHelper.test.js │ ├── GitHubURLHelper.js │ └── GitHubURLHelper.test.js └── GitLab │ ├── GitLabAPI.js │ ├── GitLabHelper.js │ ├── GitLabHelper.test.js │ ├── GitLabURLHelper.js │ └── GitLabURLHelper.test.js ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js └── setupTests.js /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '23 1 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '16.17.1' 18 | - run: yarn 19 | - run: yarn build 20 | - name: Deploy 21 | uses: peaceiris/actions-gh-pages@v3 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: ./build 25 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.10.0, 18.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: yarn 28 | - run: yarn test --ci --coverage . -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | yarn.lock 25 | #IDE files 26 | nbproject 27 | .~lock.* 28 | .buildpath 29 | .idea 30 | .project 31 | .settings 32 | .vscode 33 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: yarn 3 | command: yarn start 4 | vscode: 5 | extensions: 6 | - mhutchie.git-graph@1.28.0:8NH1WgOknx0p2byYkmBcrQ== -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/lamact/react-issue-ganttchart) 2 | 3 | react-issue-ganttchart 4 | =================== 5 | 6 | This is a single page application created with React to display github.com / gitlab.com / self-host.gitlab.com issues as a Gantt chart. 7 | No backend is required, and the token is stored in a cookie. 8 | 9 | ## Demo 10 | ![demo](demo-rig.gif) 11 | 12 | https://lamact.github.io/react-issue-ganttchart/?giturl=https%3A%2F%2Fgithub.com%2Flamact%2Freact-issue-ganttchart&labels= 13 | 14 | It is required your Personal Access Token. See below. 15 | 16 | ## Why use react-issue-ganttchart? 17 | 18 | * Only need to access GitHub/GitLab from a browser to use it, because you only need to hit the REST API against GitHub/GitLab. 19 | 20 | 21 | * Can be filtered by asignee and label to show dependencies. 22 | 23 | * Filter conditons are stored in URL parameters, so you can bookmark and save the same filter conditons. 24 | 25 | * Issues can be sorted by start date and expiration date. 26 | 27 | * Can be used on self-hosted GitLab servers. 28 | 29 | ## Requirements 30 | 31 | - Your Repository's Path (github.com / gitlab.com / self-host.gitlab.com) 32 | ex) https://github.com/lamact/react-issue-ganttchart 33 | 34 | - Personal Access Token: 35 | GitHub: https://github.com/settings/tokens/new Scopes: repo, write:discussion, read:org 36 | GitLab: https://gitlab.com/-/profile/personal_access_tokens 37 | 38 | ## How to start on your server 39 | 40 | - clone the repository or download files 41 | - install dependencies (nodejs >= 16.10.0) 42 | ~~~ 43 | yarn install 44 | ~~~ 45 | 46 | - run server 47 | ~~~ 48 | yarn start 49 | ~~~ 50 | 51 | - build and deploy for Pages 52 | ~~~ 53 | yarn deploy 54 | ~~~ 55 | 56 | # Check List 57 | 58 | | Domain | function | Result | 59 | | ------------------- | ---------------------------- | ------ | 60 | | github.com | Public Issues (List&Update) | ✅ | 61 | | github.com | Private Issues (List&Update) | ✅ | 62 | | gitlab.com | Public Issues (List&Update) | ✅ | 63 | | gitlab.com | Private Issues (List&Update) | ✅ | 64 | | gitlab.selfhost.com | Public Issues (List&Update) | - | 65 | | gitlab.selfhost.com | Private Issues (List&Update) | - | 66 | 67 | We do not test in self-hosted GitLab every time. If you find any problems, please let us know in an Issue. -------------------------------------------------------------------------------- /demo-rig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamact/react-issue-ganttchart/70a2c26fffa1011fe02a72168f54ece90cdf2e80/demo-rig.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-issue-ganttchart", 3 | "version": "0.3.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@material-ui/icons": "^4.11.2", 8 | "@material-ui/lab": "^4.0.0-alpha.57", 9 | "@testing-library/jest-dom": "^5.11.9", 10 | "@testing-library/react": "^11.2.5", 11 | "@testing-library/user-event": "^12.7.0", 12 | "axios": "^0.21.1", 13 | "dhtmlx-gantt": "^7.0.13", 14 | "gh-pages": "^3.1.0", 15 | "js-yaml": "^4.0.0", 16 | "moment": "^2.29.1", 17 | "multiselect-react-dropdown": "^1.6.7", 18 | "react": "^17.0.1", 19 | "react-dom": "^17.0.1", 20 | "react-helmet": "^6.1.0", 21 | "react-hook-form": "^6.15.1", 22 | "react-markdown": "^5.0.3", 23 | "react-router-dom": "^5.2.0", 24 | "react-scripts": "4.0.2", 25 | "sfcookies": "^1.0.2" 26 | }, 27 | "homepage": "https://lamact.github.io/react-issue-ganttchart/", 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject", 33 | "deploy": "yarn build && gh-pages -d build" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "axios-mock-adapter": "^1.19.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamact/react-issue-ganttchart/70a2c26fffa1011fe02a72168f54ece90cdf2e80/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Issue Gantt 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Issue Gantt Chart", 3 | "name": "Lamact React Issue Gantt Chart", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamact/react-issue-ganttchart/70a2c26fffa1011fe02a72168f54ece90cdf2e80/src/App.css -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, useEffect } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import Toolbar from './components/Toolbar'; 4 | import Gantt from './components/Gantt'; 5 | import PageHeader from './components/PageHeader'; 6 | import { read_cookie } from 'sfcookies'; 7 | import { withRouter } from 'react-router-dom'; 8 | import { initialState, reducerFunc } from './State/Reducer.js'; 9 | import { 10 | getIssuesFromAPI, 11 | setLabelListOfRepoFromAPI, 12 | setMemberListOfRepoFromAPI, 13 | } from './functions/Common/IssueAPI.js'; 14 | import { gantt } from 'dhtmlx-gantt'; 15 | 16 | const App = (props) => { 17 | const [state, dispatch] = useReducer(reducerFunc, initialState); 18 | const { register, setValue } = useForm({ git_url: '', token: '' }); 19 | 20 | useEffect(() => { 21 | setValue('token', read_cookie('git_token')); 22 | dispatch({ type: 'tokenChange', value: read_cookie('git_token') }); 23 | 24 | // Set Zoom Level 25 | const zoom = read_cookie("zoom") 26 | if (zoom == "Days" || zoom == "Weeks" || zoom == "Years") { 27 | gantt.ext.zoom.setLevel(zoom); 28 | } 29 | 30 | // Set Menu Opened 31 | gantt.config.show_grid = read_cookie('menu_opened'); 32 | gantt.render(); 33 | }, []); 34 | 35 | useEffect(() => { 36 | dispatch({ 37 | type: 'setStateFromURLQueryString', 38 | value: { props: props, setValue: setValue }, 39 | }); 40 | }, [props.location]); 41 | 42 | useEffect(() => { 43 | setLabelListOfRepoFromAPI(state.git_url, state.token) 44 | .then((labels) => { 45 | dispatch({ type: 'labelChange', value: labels }); 46 | }) 47 | .catch((err) => { 48 | gantt.message({ text: err, type: 'error' }); 49 | }); 50 | setMemberListOfRepoFromAPI(state.git_url, state.token) 51 | .then((members) => { 52 | dispatch({ type: 'memberListChange', value: members }); 53 | }) 54 | .catch((err) => { 55 | gantt.message({ text: err, type: 'error' }); 56 | }); 57 | }, [state.git_url, state.token, state.selected_assignee]); 58 | 59 | useEffect(() => { 60 | //dispatch({ type: 'getIssueByAPI' }); 61 | getIssuesFromAPI( 62 | state.git_url, 63 | state.token, 64 | state.selected_labels, 65 | state.selected_assignee 66 | ) 67 | .then((issues) => { 68 | dispatch({ type: 'setIssue', value: issues }); 69 | }) 70 | .catch((err) => { 71 | console.log('error'); 72 | }); 73 | }, [ 74 | state.git_url, 75 | state.token, 76 | state.selected_labels, 77 | state.selected_assignee, 78 | ]); 79 | 80 | return ( 81 | <> 82 | 83 |
84 | dispatch({ type: 'zoomChange', value: zoom })} 87 | onGitURLChange={(git_url) => 88 | dispatch({ 89 | type: 'gitURLChange', 90 | value: { props: props, git_url: git_url }, 91 | }) 92 | } 93 | token={state.token} 94 | onTokenChange={(token) => 95 | dispatch({ type: 'tokenChange', value: token }) 96 | } 97 | labels={state.labels} 98 | selected_labels={state.selected_labels} 99 | onSelectedLabelChange={(selected_labels) => 100 | dispatch({ 101 | type: 'selectedLabelsChange', 102 | value: { props: props, selected_labels: selected_labels }, 103 | }) 104 | } 105 | member_list={state.member_list} 106 | selected_assignee={state.selected_assignee} 107 | onSelectedAssigneeChange={(selected_assignee) => 108 | dispatch({ 109 | type: 'selectedAssigneeChange', 110 | value: { props: props, selected_assignee: selected_assignee }, 111 | }) 112 | } 113 | register={register} 114 | /> 115 |
116 |
117 | 125 | dispatch({ type: 'openIssueAtBrowser', value: gantt_task_id }) 126 | } 127 | openNewIssueAtBrowser={(gantt_task_id) => 128 | dispatch({ type: 'openNewIssueAtBrowser', value: gantt_task_id }) 129 | } 130 | updateIssueByAPI={(gantt_task, gantt) => 131 | dispatch({ 132 | type: 'updateIssueByAPI', 133 | value: { gantt_task: gantt_task, gantt: gantt }, 134 | }) 135 | } 136 | /> 137 |
138 | 139 | ); 140 | }; 141 | 142 | export default withRouter(App); 143 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | 2 | describe('true is truthy and false is falsy', () => { 3 | test('true is truthy', () => { 4 | expect(true).toBe(true); 5 | }); 6 | 7 | test('false is falsy', () => { 8 | expect(false).toBe(false); 9 | }); 10 | }); -------------------------------------------------------------------------------- /src/State/Reducer.js: -------------------------------------------------------------------------------- 1 | import { bake_cookie } from 'sfcookies'; 2 | import { 3 | convertIDNamesStringToList, 4 | convertIDNameListToString, 5 | removeLastSlash, 6 | removeLastSpace, 7 | } from '../functions/Common/Parser.js'; 8 | import { 9 | updateIssueByAPI, 10 | openIssueAtBrowser, 11 | openNewIssueAtBrowser, 12 | } from '../functions/Common/IssueAPI.js'; 13 | import { isValidVariable } from '../functions/Common/CommonHelper.js'; 14 | import { 15 | isGitHubURL, 16 | getGitHubProjectFromGitURL, 17 | } from '../functions/GitHub/GitHubURLHelper.js'; 18 | import { 19 | isGitLabURL, 20 | getSelfHostingGitLabDomain, 21 | getGitLabProjectFromGitURL, 22 | } from '../functions/GitLab/GitLabURLHelper.js'; 23 | 24 | import { gantt } from 'dhtmlx-gantt'; 25 | 26 | export const initialState = { 27 | currentZoom: 'Days', 28 | git_url: '', 29 | token: 'Tokens that have not yet been entered', 30 | labels: [], 31 | selected_labels: [], 32 | member_list: [], 33 | selected_assignee: {}, 34 | issue: [], 35 | issue_columns: [], 36 | title: '', 37 | }; 38 | 39 | export const reducerFunc = (state, action) => { 40 | switch (action.type) { 41 | case 'zoomChange': 42 | return { ...state, currentZoom: handleZoomChange(action.value) }; 43 | case 'gitURLChange': 44 | return handleGitURLChange(state, action); 45 | case 'tokenChange': 46 | return { ...state, token: handleTokenChange(action.value) }; 47 | case 'labelChange': 48 | return { ...state, labels: action.value }; 49 | case 'selectedLabelsChange': 50 | return { 51 | ...state, 52 | selected_labels: handleSelectedLabelsChange( 53 | action.value.props, 54 | state.git_url, 55 | action.value.selected_labels, 56 | state.selected_assignee 57 | ), 58 | }; 59 | case 'memberListChange': 60 | return { ...state, member_list: handleMemberListChange(action.value) }; 61 | case 'selectedAssigneeChange': 62 | return { 63 | ...state, 64 | selected_assignee: handleSelectedAssigneeChange( 65 | action.value.props, 66 | state.git_url, 67 | state.selected_labels, 68 | action.value.selected_assignee 69 | ), 70 | }; 71 | case 'openIssueAtBrowser': 72 | return handleOpenIssueAtBrowser(state, action); 73 | case 'openNewIssueAtBrowser': 74 | return handleOpenNewIssueAtBrowser(state, action); 75 | case 'updateIssueByAPI': 76 | return handleUpdateIssueByAPI(state, action); 77 | case 'setIssue': 78 | return { ...state, issue: action.value }; 79 | case 'setStateFromURLQueryString': 80 | return setStateFromURLQueryString( 81 | state, 82 | action.value.props, 83 | action.value.setValue 84 | ); 85 | default: 86 | return state; 87 | } 88 | }; 89 | 90 | export const handleZoomChange = (zoom) => { 91 | bake_cookie('zoom', zoom); 92 | return zoom; 93 | }; 94 | 95 | export const handleOpenIssueAtBrowser = (state, action) => { 96 | openIssueAtBrowser(action.value, state.git_url); 97 | return state; 98 | }; 99 | 100 | export const handleOpenNewIssueAtBrowser = (state, action) => { 101 | openNewIssueAtBrowser(action.value, state.git_url); 102 | return state; 103 | }; 104 | 105 | export const handleUpdateIssueByAPI = (state, action) => { 106 | updateIssueByAPI( 107 | action.value.gantt_task, 108 | state.token, 109 | action.value.gantt, 110 | state.git_url 111 | ); 112 | return state; 113 | }; 114 | 115 | export const handleGitURLChange = (state, action) => { 116 | const git_url = removeLastSlash(removeLastSpace(action.value.git_url)); 117 | if (isGitHubURL(git_url)) { 118 | gantt.message({ text: 'Access GitHub.com' }); 119 | state.title = getGitHubProjectFromGitURL(git_url); 120 | } else if (isGitLabURL(git_url)) { 121 | gantt.message({ text: 'Access GitLab.com' }); 122 | state.title = getGitLabProjectFromGitURL(git_url); 123 | } else if (getSelfHostingGitLabDomain(git_url) !== null) { 124 | gantt.message({ text: 'Access Maybe GitLab.self-host' }); 125 | state.title = getGitLabProjectFromGitURL(git_url); 126 | } else if (git_url === '') { 127 | } else { 128 | gantt.message({ text: 'Not a valid URL.', type: 'error' }); 129 | return null; 130 | } 131 | setURLQuery( 132 | action.value.props, 133 | git_url, 134 | state.selected_labels, 135 | action.value.selected_assignee 136 | ); 137 | state.git_url = git_url 138 | return state; 139 | }; 140 | 141 | export const handleTokenChange = (token) => { 142 | bake_cookie('git_token', token); 143 | return token; 144 | }; 145 | 146 | export const handleSelectedLabelsChange = ( 147 | props, 148 | git_url, 149 | selected_labels, 150 | selected_assignee 151 | ) => { 152 | setURLQuery(props, git_url, selected_labels, selected_assignee); 153 | return selected_labels; 154 | }; 155 | 156 | export const handleMemberListChange = ( 157 | member_list 158 | ) => { 159 | if (isValidVariable(member_list)) { 160 | return member_list; 161 | } else { 162 | return []; 163 | } 164 | }; 165 | 166 | export const handleSelectedAssigneeChange = ( 167 | props, 168 | git_url, 169 | selected_labels, 170 | selected_assignee 171 | ) => { 172 | setURLQuery(props, git_url, selected_labels, selected_assignee); 173 | return selected_assignee; 174 | }; 175 | 176 | export const setURLQuery = (props, git_url, selected_labels, selected_assignee) => { 177 | const params = new URLSearchParams(props.location.search); 178 | params.set('giturl', git_url); 179 | params.set('labels', convertIDNameListToString(selected_labels)); 180 | params.set('assignee', convertIDNameListToString([selected_assignee])); 181 | props.history.push({ 182 | search: params.toString(), 183 | }); 184 | return null; 185 | }; 186 | 187 | export const setStateFromURLQueryString = (state, props, setValue) => { 188 | const params = new URLSearchParams(props.location.search); 189 | state.git_url = params.get('giturl'); 190 | 191 | const git_url = removeLastSlash(removeLastSpace(params.get('giturl'))); 192 | if (isGitHubURL(git_url)) { 193 | state.title = getGitHubProjectFromGitURL(git_url); 194 | } else if (isGitLabURL(git_url)) { 195 | state.title = getGitLabProjectFromGitURL(git_url); 196 | } else if (getSelfHostingGitLabDomain(git_url) !== null) { 197 | state.title = getGitLabProjectFromGitURL(git_url); 198 | } 199 | state.git_url = git_url; 200 | 201 | const selected_labels = convertIDNamesStringToList(params.get('labels')); 202 | if (isValidVariable(selected_labels[0])) { 203 | if ('name' in selected_labels[0]) { 204 | if (selected_labels[0].name !== '') { 205 | state.selected_labels = selected_labels; 206 | } 207 | } 208 | } 209 | 210 | const selected_assignee_list = convertIDNamesStringToList( 211 | params.get('assignee') 212 | ); 213 | if (isValidVariable(selected_assignee_list)) { 214 | state.selected_assignee = selected_assignee_list[0]; 215 | } 216 | setValue('git_url', state.git_url); 217 | return state; 218 | }; 219 | -------------------------------------------------------------------------------- /src/State/Reducer.test.js: -------------------------------------------------------------------------------- 1 | import { handleOpenIssueAtBrowser } from "./Reducer"; 2 | 3 | 4 | const initialState = { 5 | currentZoom: 'Weeks', 6 | update: 0, 7 | git_url: '', 8 | token: 'Tokens that have not yet been entered', 9 | labels: [], 10 | selected_labels: [], 11 | member_list: [], 12 | selected_assignee: {}, 13 | }; 14 | 15 | describe('handleOpenIssueAtBrowser', () => { 16 | test('true', () => { 17 | expect(handleOpenIssueAtBrowser(initialState, {value: "A"})).toEqual( 18 | initialState 19 | ); 20 | }); 21 | }); -------------------------------------------------------------------------------- /src/components/Gantt/Gantt.css: -------------------------------------------------------------------------------- 1 | .gantt-container { 2 | height: calc(100vh - 50px); 3 | } 4 | .past_days { 5 | background: #8b8b8b13; 6 | } 7 | .today { 8 | background: #60daffa6; 9 | } 10 | .weekend { 11 | background: #8b8b8b13; 12 | } 13 | 14 | .behind .gantt_task_progress { 15 | background-color: #ff7777; 16 | } 17 | .behind .gantt_task_progress_wrapper { 18 | background: #ff8b8b; 19 | } 20 | 21 | .overdue { 22 | display: inline-block; 23 | width: 16px; 24 | height: 16px; 25 | border-radius: 50%; 26 | background: rgb(151, 149, 149); 27 | text-align: center; 28 | line-height: 16px; 29 | color: white; 30 | margin-left: 5px; 31 | } 32 | 33 | .buttonBg { 34 | background: #fff; 35 | } 36 | .gridHoverStyle { 37 | background-color: #ffe6b1 !important; 38 | background-color: #ffebc1; 39 | background-image: linear-gradient(0deg, #ffe09d 0, #ffeabb); 40 | border-top-color: #ffc341; 41 | border-bottom-color: #ffc341; 42 | } 43 | .gridSelection, 44 | .timelineSelection { 45 | background-color: #ffe6b1 !important; 46 | border-bottom-color: #ff4141; 47 | } 48 | .timelineSelection { 49 | background-color: #ffebc1; 50 | background-image: linear-gradient(0deg, #ffe09d 0, #ffeabb); 51 | border-top-color: #ffc341; 52 | } 53 | .timelineSelection .gantt_task_cell { 54 | border-right-color: #ffce65; 55 | } 56 | .gantt_popup_shadow { 57 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.07); 58 | } 59 | .gantt_cal_quick_info .gantt_cal_qi_title { 60 | background: #fff; 61 | } 62 | .gantt_cal_qi_controls .gantt_qi_big_icon .gantt_menu_icon.icon_delete { 63 | margin-top: 5px; 64 | } 65 | .gantt_popup_title { 66 | box-shadow: inset 0 1px 1px #fff; 67 | background-color: #dfedff; 68 | background: -webkit-gradient( 69 | linear, 70 | left top, 71 | left bottom, 72 | color-stop(0, #e4f0ff), 73 | color-stop(50%, #dfedff), 74 | color-stop(100%, #d5e8ff) 75 | ); 76 | background-image: -o-linear-gradient( 77 | top, 78 | #e4f0ff 0, 79 | #dfedff 60%, 80 | #d5e8ff 100% 81 | ); 82 | background-position: 0 1px; 83 | background-repeat: repeat-x; 84 | } 85 | .gantt_tooltip { 86 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.07); 87 | border-left: 1px solid rgba(0, 0, 0, 0.07); 88 | border-top: 1px solid rgba(0, 0, 0, 0.07); 89 | font-size: 8pt; 90 | color: #1e2022; 91 | } 92 | .gantt_container, 93 | .gantt_tooltip { 94 | background-color: #fff; 95 | font-family: Tahoma; 96 | } 97 | .gantt_container { 98 | font-size: 11px; 99 | border: 1px solid #a4bed4; 100 | position: relative; 101 | white-space: nowrap; 102 | overflow-x: hidden; 103 | overflow-y: hidden; 104 | } 105 | .gantt_touch_active { 106 | overscroll-behavior: none; 107 | } 108 | .gantt_task_scroll { 109 | overflow-x: scroll; 110 | } 111 | .gantt_grid, 112 | .gantt_task { 113 | position: relative; 114 | overflow-x: hidden; 115 | overflow-y: hidden; 116 | display: inline-block; 117 | vertical-align: top; 118 | } 119 | .gantt_grid_scale, 120 | .gantt_task_scale { 121 | color: #42464b; 122 | border-bottom: 1px solid #a4bed4; 123 | box-sizing: border-box; 124 | } 125 | .gantt_grid_scale, 126 | .gantt_task_scale, 127 | .gantt_task_vscroll { 128 | box-shadow: inset 0 1px 1px #fff; 129 | background-color: #dfedff; 130 | background: -webkit-gradient( 131 | linear, 132 | left top, 133 | left bottom, 134 | color-stop(0, #e4f0ff), 135 | color-stop(50%, #dfedff), 136 | color-stop(100%, #d5e8ff) 137 | ); 138 | background-image: -o-linear-gradient( 139 | top, 140 | #e4f0ff 0, 141 | #dfedff 60%, 142 | #d5e8ff 100% 143 | ); 144 | background-position: 0 1px; 145 | background-repeat: repeat-x; 146 | } 147 | .gantt_scale_line { 148 | box-sizing: border-box; 149 | -moz-box-sizing: border-box; 150 | border-top: 1px solid #a4bed4; 151 | } 152 | .gantt_scale_line:first-child { 153 | border-top: none; 154 | } 155 | .gantt_grid_head_cell { 156 | display: inline-block; 157 | vertical-align: top; 158 | border-right: 1px solid #a4bed4; 159 | text-align: center; 160 | position: relative; 161 | cursor: default; 162 | height: 100%; 163 | box-sizing: border-box; 164 | -moz-box-sizing: border-box; 165 | line-height: 25px; 166 | -moz-user-select: -moz-none; 167 | -webkit-user-select: none; 168 | user-select: none; 169 | overflow: hidden; 170 | } 171 | .gantt_scale_line { 172 | clear: both; 173 | } 174 | .gantt_grid_data { 175 | width: 100%; 176 | overflow: hidden; 177 | position: relative; 178 | } 179 | .gantt_row { 180 | position: relative; 181 | -webkit-user-select: none; 182 | -moz-user-select: none; 183 | -moz-user-select: -moz-none; 184 | } 185 | .gantt_add, 186 | .gantt_grid_head_add { 187 | width: 100%; 188 | height: 100%; 189 | /* background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzZCMThBRUFCRTQ0MTFFMkFFMEFGMEFBMzJEN0RBRTIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzZCMThBRUJCRTQ0MTFFMkFFMEFGMEFBMzJEN0RBRTIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NkIxOEFFOEJFNDQxMUUyQUUwQUYwQUEzMkQ3REFFMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NkIxOEFFOUJFNDQxMUUyQUUwQUYwQUEzMkQ3REFFMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkVHygEAAAEpSURBVHjaYgzeGcyABfxnwA4Y0QVY/v79i1VlsUMxCr/3QC9WdTgN+P//P8Ohr4fAbDtuOwZc6lj+/PmDVeLfv39wTSA2LnU4DQC5AGYAiI1LHRNQ4j861pfSZ3j36x1YEwiD2CAxbGoZbRbZ/K/3qUcx9cPvDwzXv11n+PL3C5jPw8zDoMmlySDAKoCirnFLI8QLID/ufLOTARf48OcDw/Gfx1HE3EXcwa5j+f37N95AwgVAekB64QaATISB97/fM1z5dIXh85/PYD4vCy+DDp8OgyCrIIYBjFoTtDBSnYWyBYO9rj3DsbfHwHwrYSuGg5cPMpy4ewIzFoCmMKLjwzcOMwiwCMBjAcQGiWFTizchweRompDwuiBANoCgCxjFasQoys4AAQYARt4I/K036xQAAAAASUVORK5CYII=); */ 190 | background-position: 50%; 191 | background-repeat: no-repeat; 192 | cursor: pointer; 193 | position: relative; 194 | -moz-opacity: 0.3; 195 | opacity: 0.3; 196 | } 197 | .gantt_grid_head_cell.gantt_grid_head_add { 198 | -moz-opacity: 0.6; 199 | opacity: 0.6; 200 | top: 0; 201 | } 202 | .gantt_grid_head_cell.gantt_grid_head_add:hover { 203 | -moz-opacity: 1; 204 | opacity: 1; 205 | } 206 | .gantt_grid_data .gantt_row.odd:hover, 207 | .gantt_grid_data .gantt_row:hover { 208 | background-color: #ffe6b1 !important; 209 | background-color: #ffebc1; 210 | background-image: linear-gradient(0deg, #ffe09d 0, #ffeabb); 211 | border-top-color: #a16e00; 212 | border-bottom-color: #ffc341; 213 | } 214 | .gantt_grid_data .gantt_row.odd:hover .gantt_add, 215 | .gantt_grid_data .gantt_row:hover .gantt_add { 216 | -moz-opacity: 1; 217 | opacity: 1; 218 | } 219 | .gantt_row, 220 | .gantt_task_row { 221 | border-bottom: 1px solid #ebebeb; 222 | background-color: #fff; 223 | } 224 | .gantt_row.odd, 225 | .gantt_task_row.odd { 226 | background-color: #fff; 227 | } 228 | .gantt_cell, 229 | .gantt_grid_head_cell, 230 | .gantt_row, 231 | .gantt_scale_cell, 232 | .gantt_task_cell, 233 | .gantt_task_row { 234 | box-sizing: border-box; 235 | -moz-box-sizing: border-box; 236 | } 237 | .gantt_grid_head_cell, 238 | .gantt_scale_cell { 239 | line-height: inherit; 240 | } 241 | .gantt_grid_scale .gantt_grid_column_resize_wrap { 242 | cursor: col-resize; 243 | position: absolute; 244 | width: 13px; 245 | margin-left: -7px; 246 | } 247 | .gantt_grid_column_resize_wrap .gantt_grid_column_resize { 248 | background-color: #a4bed4; 249 | height: 100%; 250 | width: 1px; 251 | margin: 0 auto; 252 | } 253 | .gantt_drag_marker.gantt_grid_resize_area { 254 | background-color: hsla(0, 0%, 91%, 0.5); 255 | border-left: 1px solid #a4bed4; 256 | border-right: 1px solid #a4bed4; 257 | height: 100%; 258 | width: 100%; 259 | box-sizing: border-box; 260 | } 261 | .gantt_row { 262 | display: flex; 263 | } 264 | .gantt_row > div { 265 | flex-shrink: 0; 266 | flex-grow: 0; 267 | } 268 | .gantt_cell { 269 | vertical-align: top; 270 | border-right: 1px solid #ebebeb; 271 | padding-left: 6px; 272 | padding-right: 6px; 273 | height: 100%; 274 | overflow: hidden; 275 | white-space: nowrap; 276 | } 277 | .gantt_cell_tree { 278 | display: flex; 279 | flex-wrap: nowrap; 280 | } 281 | .gantt_grid_data .gantt_last_cell, 282 | .gantt_grid_scale .gantt_last_cell, 283 | .gantt_task .gantt_task_scale .gantt_scale_cell.gantt_last_cell, 284 | .gantt_task_bg .gantt_last_cell { 285 | border-right-width: 0; 286 | } 287 | .gantt_task .gantt_task_scale .gantt_scale_cell.gantt_last_cell { 288 | border-right-width: 1px; 289 | } 290 | .gantt_task_bg { 291 | overflow: hidden; 292 | } 293 | .gantt_scale_cell { 294 | display: inline-block; 295 | white-space: nowrap; 296 | overflow: hidden; 297 | border-right: 1px solid #a4bed4; 298 | text-align: center; 299 | height: 100%; 300 | } 301 | .gantt_task_cell { 302 | display: inline-block; 303 | height: 100%; 304 | border-right: 1px solid #ebebeb; 305 | } 306 | .gantt_layout_cell.gantt_ver_scroll { 307 | width: 0; 308 | background-color: transparent; 309 | height: 1px; 310 | overflow-x: hidden; 311 | overflow-y: scroll; 312 | position: absolute; 313 | right: 0; 314 | z-index: 1; 315 | } 316 | .gantt_ver_scroll > div { 317 | width: 1px; 318 | height: 1px; 319 | } 320 | .gantt_hor_scroll { 321 | height: 0; 322 | background-color: transparent; 323 | width: 100%; 324 | clear: both; 325 | overflow-x: scroll; 326 | overflow-y: hidden; 327 | } 328 | .gantt_layout_cell .gantt_hor_scroll { 329 | position: absolute; 330 | } 331 | .gantt_hor_scroll > div { 332 | width: 5000px; 333 | height: 1px; 334 | } 335 | .gantt_tree_icon, 336 | .gantt_tree_indent { 337 | flex-grow: 0; 338 | flex-shrink: 0; 339 | } 340 | .gantt_tree_indent { 341 | width: 15px; 342 | height: 100%; 343 | } 344 | .gantt_tree_content, 345 | .gantt_tree_icon { 346 | vertical-align: top; 347 | } 348 | .gantt_tree_icon { 349 | width: 28px; 350 | height: 100%; 351 | background-repeat: no-repeat; 352 | background-position: 50%; 353 | } 354 | .gantt_tree_content { 355 | height: 100%; 356 | white-space: nowrap; 357 | min-width: 0; 358 | } 359 | .gantt_tree_icon.gantt_open { 360 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAWUlEQVQ4T2NkoBJgpJI5DCPVoJaWlv+EwrCmpgYcPHjDCGRQdXU1TrNaW1sZyDKIkZGR4f9/hCMHziCQS9AByGUD5yKYa6gWRujeI8lrVElHhAxBlh/GmRYA5MZIE4VV1O8AAAAASUVORK5CYII=); 361 | width: 18px; 362 | cursor: pointer; 363 | } 364 | .gantt_tree_icon.gantt_close { 365 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAATElEQVQ4T2NkoBJgpJI5DCPVoJaWlv+EwrCmpgYcPHjDCGRQdXU1TrNaW1sZhrhBjIyYIfD//3+GYeA1XNFGkteoko4IGYIsP4wzLQBu8jwTv5HhogAAAABJRU5ErkJggg==); 366 | width: 18px; 367 | cursor: pointer; 368 | } 369 | .gantt_tree_icon.gantt_blank { 370 | width: 18px; 371 | } 372 | /* .gantt_tree_icon.gantt_folder_open { 373 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAB30lEQVQ4T2NkQAKnZyn9R+bjY5um3WNElodz0A0xSbqMas6/nwzf/3AwcLL8YPjxYhvDlR11DMiGoRgkp63OIGY0ieHPt7cMF9ZGwQ0yjj0EZv/4L8TAwfiO4f+3awzn1mbgNsgkdidEM6sU3JA/Hy+jGhq+jOH/n09EGoTsKZihQK+dWWDCYBQ8AyxLnIugBv3/9RbMArmAkUuL4exiOwYD/26w2IWNpbi9Zhy5gYHh3zewxn9/PjMw/3nDwPD7HQODsDdY89nlAQw6Hk1gNt7ABjkb2YD/f78w/P3xloFFLoth8cRsBi2xp8QZZOxdDnYBzACQzb+/f2BgUywAB7iyaRA89DiFDVBchRL9Rs6RYBfADADRf398ZuDUrIUYZB6GiIZ/fxh4pP0YLm5IAIcVikH61m4YhjD++cbArtUI1oDson9/fjDwqqQxXFodgGmQlpEZ2CCQKyAmfgIG/i8GDt1u0gxS19IE6we54v//P2BDQABmkKKhF8JrQBZOF4EMghsC0kKuQZICXxj4xbQhroECRkYWoIvawYEtp20NF2dm42dgk4ljuLk5AjWMQCpgJQDMQJgumEEo/oJyYCUASpkCU4hcpIAMFXfeBHYRehmEbDBWg5AVIBuKzyAAfjsXIj7TBm0AAAAASUVORK5CYII=); 374 | } 375 | .gantt_tree_icon.gantt_folder_closed { 376 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAB90lEQVQ4T2NkQAKnZyn9R+bjY5um3WNElodzQIboWYUwsGnU4tR/Zp4+g6J1NQM/HzvDhe11DMiGoRhkErMHiyHMEDE2IQaQQZwcHAxqXvMZLq6LJMUgiCH//3xlYOSSJccgqAG/3oAN+vv7GwMLtxzDmSWOYBepe85kuLA+npCLmMEuYPj3neHfjxcQF/1+z8AibAU0yAVskIZbN8P5Tbm4DTKO2AI2AOQCxr+fwAb8+/2JgVXEAWzg2RU+EBc5VuEPbIPAhSgGMPwFuuznawZWuQSGxZPSGbTEnoINUrXJYbi0pwe3iww8m8AugBkAcsXv728Y2FXyGC6sDmJgZmZmYGNlZVC1TGRgYOZGMQw1HTlmgl0AM+D/ny9gNrtqMTi6QQaJC3Mw8Mm5gcU5pZzgYYWRIH/9+MzA+P8nMMC/AMPoK8O/P78YuHXrwLHEyMzGICnMCjHo3zcGDuV0sEtBCRPFIE1DB7ABkJiCGPL/3y8GHv0WuEEi/CwMwkru+A3S0DWBGwA2DGgICGAYpGALFsfpInUNTYa//xgYGKEGwAzjNugChxHIayAXicibMfxnZMdtkKTANwYeEV2G/0xsYMNANDjgJRIYHh4qhwQwMPrlVXUYGFl4GL7xeoLFUcIIpIiUYgRsKhDASgCUMgUmSQ5NNYMAK+sKIlqSof0AAAAASUVORK5CYII=); 377 | } 378 | .gantt_tree_icon.gantt_file { 379 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAA/klEQVQ4T63Uyw5FMBAG4CliIcHOE9pIeBrCxou69py/yUgvnLRymogS85lOL4L+1ITu1HUtfdx5no04xFwvgPR9T3EckxDOd8rf913du64jGzOgcRxpWRYDQvB5nrSuK5VleSVsYwY0TRNt20ZRFKlgzuI4DgUVRWGMXMccCEHcgEkp1ZCQaZ7n/hACuUboA8YFDNkCR7+qKmqa5qqVk5E9azrGKDDUKwgCzBgAYHjOsiwcAsb14hqmafoOQhY8k+gnSeIPIYAb9xl7Bekgw1hrQcW+Q9Te+m6jIOjXJn6EhmFQm9anYfbatnUXJIJ9jxH+kX4C3J8XPilZ33wAwx73EyvYLYIAAAAASUVORK5CYII=); 380 | } */ 381 | .gantt_grid_head_cell .gantt_sort { 382 | position: absolute; 383 | right: 5px; 384 | top: 8px; 385 | width: 7px; 386 | height: 13px; 387 | background-repeat: no-repeat; 388 | background-position: 50%; 389 | } 390 | .gantt_grid_head_cell .gantt_sort.gantt_asc { 391 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAANCAYAAABlyXS1AAAARUlEQVR4nGNgQAKGxib/GbABkIS7b8B/DAUwCRiGK0CXwFBAb1DfP/U/LszwHwi2X7qFgUEArBtdAVwCBmAKMCSQFSDzAWXXaOHsXeqkAAAAAElFTkSuQmCC); 392 | } 393 | .gantt_grid_head_cell .gantt_sort.gantt_desc { 394 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAANCAYAAABlyXS1AAAARUlEQVR42mNgQAL1/VP/M2ADIIntF2/9x1AAlrh0C47hCmA60DFYwX88gIFGwNDY5D8uDFbg7hvwHx2jmIBTAlkB0e4BAEjlaNtBWJPnAAAAAElFTkSuQmCC); 395 | } 396 | .gantt_inserted, 397 | .gantt_updated { 398 | font-weight: 700; 399 | } 400 | .gantt_deleted { 401 | text-decoration: line-through; 402 | } 403 | .gantt_invalid { 404 | background-color: #ffe0e0; 405 | } 406 | .gantt_error { 407 | color: red; 408 | } 409 | .gantt_status { 410 | right: 1px; 411 | padding: 5px 10px; 412 | background: hsla(0, 0%, 61%, 0.1); 413 | position: absolute; 414 | top: 1px; 415 | transition: opacity 0.2s; 416 | opacity: 0; 417 | } 418 | .gantt_status.gantt_status_visible { 419 | opacity: 1; 420 | } 421 | #gantt_ajax_dots span { 422 | transition: opacity 0.2s; 423 | background-repeat: no-repeat; 424 | opacity: 0; 425 | } 426 | #gantt_ajax_dots span.gantt_dot_visible { 427 | opacity: 1; 428 | } 429 | .gantt_column_drag_marker { 430 | border: 1px solid #cecece; 431 | opacity: 0.8; 432 | } 433 | .gantt_grid_head_cell_dragged { 434 | border: 1px solid #cecece; 435 | opacity: 0.3; 436 | } 437 | .gantt_grid_target_marker { 438 | position: absolute; 439 | top: 0; 440 | width: 2px; 441 | height: 100%; 442 | background-color: #4a8f43; 443 | transform: translateX(-1px); 444 | } 445 | .gantt_grid_target_marker:after, 446 | .gantt_grid_target_marker:before { 447 | display: block; 448 | content: ''; 449 | position: absolute; 450 | left: -5px; 451 | width: 0; 452 | height: 0; 453 | border: 6px solid transparent; 454 | } 455 | .gantt_grid_target_marker:before { 456 | border-top-color: #4a8f43; 457 | } 458 | .gantt_grid_target_marker:after { 459 | bottom: 0; 460 | border-bottom-color: #4a8f43; 461 | } 462 | .gantt_message_area { 463 | position: fixed; 464 | right: 5px; 465 | width: 250px; 466 | z-index: 1000; 467 | } 468 | .gantt-info { 469 | min-width: 120px; 470 | padding: 4px 4px 4px 20px; 471 | font-family: Tahoma; 472 | z-index: 10000; 473 | margin: 5px; 474 | margin-bottom: 10px; 475 | transition: all 0.5s ease; 476 | } 477 | .gantt-info.hidden { 478 | height: 0; 479 | padding: 0; 480 | border-width: 0; 481 | margin: 0; 482 | overflow: hidden; 483 | } 484 | .gantt_modal_box { 485 | overflow: hidden; 486 | display: inline-block; 487 | min-width: 250px; 488 | width: 250px; 489 | text-align: center; 490 | position: fixed; 491 | z-index: 20000; 492 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.07); 493 | font-family: Tahoma; 494 | border-radius: 0; 495 | border: 1px solid #a4bed4; 496 | background: #fff; 497 | } 498 | .gantt_popup_title { 499 | border-top-left-radius: 0; 500 | border-top-right-radius: 0; 501 | border-width: 0; 502 | } 503 | .gantt_button, 504 | .gantt_popup_button { 505 | border: 1px solid #a4bed4; 506 | height: 24px; 507 | line-height: 24px; 508 | display: inline-block; 509 | margin: 0 5px; 510 | border-radius: 4px; 511 | background: #fff; 512 | background-color: #f8f8f8; 513 | background-image: linear-gradient(0deg, #e6e6e6 0, #fff); 514 | } 515 | .gantt-info, 516 | .gantt_button, 517 | .gantt_popup_button { 518 | user-select: none; 519 | -webkit-user-select: none; 520 | -moz-user-select: -moz-none; 521 | cursor: pointer; 522 | } 523 | .gantt_popup_text { 524 | overflow: hidden; 525 | } 526 | .gantt_popup_controls { 527 | border-radius: 6px; 528 | padding: 10px; 529 | } 530 | .gantt_popup_button { 531 | min-width: 100px; 532 | } 533 | div.dhx_modal_cover { 534 | background-color: #000; 535 | cursor: default; 536 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=20); 537 | opacity: 0.2; 538 | position: fixed; 539 | z-index: 19999; 540 | left: 0; 541 | top: 0; 542 | width: 100%; 543 | height: 100%; 544 | border: none; 545 | zoom: 1; 546 | } 547 | .gantt-info img, 548 | .gantt_modal_box img { 549 | float: left; 550 | margin-right: 20px; 551 | } 552 | .gantt-alert-error, 553 | .gantt-confirm-error { 554 | border: 1px solid red; 555 | } 556 | .gantt_button input, 557 | .gantt_popup_button div { 558 | border-radius: 4px; 559 | font-size: 15px; 560 | box-sizing: content-box; 561 | padding: 0; 562 | margin: 0; 563 | vertical-align: top; 564 | } 565 | .gantt_popup_title { 566 | border-bottom: 1px solid #a4bed4; 567 | height: 40px; 568 | line-height: 40px; 569 | font-size: 20px; 570 | } 571 | .gantt_popup_text { 572 | margin: 15px 15px 5px; 573 | font-size: 14px; 574 | color: #000; 575 | min-height: 30px; 576 | border-radius: 0; 577 | } 578 | .gantt-error, 579 | .gantt-info { 580 | font-size: 14px; 581 | color: #000; 582 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.07); 583 | padding: 0; 584 | background-color: #fff; 585 | border-radius: 3px; 586 | border: 1px solid #fff; 587 | } 588 | .gantt-info div { 589 | padding: 5px 10px; 590 | background-color: #fff; 591 | border-radius: 3px; 592 | border: 1px solid #a4bed4; 593 | } 594 | .gantt-error { 595 | background-color: #ff4b4b; 596 | border: 1px solid #ff3c3c; 597 | } 598 | .gantt-error div { 599 | background-color: #d81b1b; 600 | border: 1px solid #940000; 601 | color: #fff; 602 | } 603 | .gantt-warning { 604 | background-color: #ffa000; 605 | border: 1px solid #ffb333; 606 | } 607 | .gantt-warning div { 608 | background-color: #ffa000; 609 | border: 1px solid #b37000; 610 | color: #fff; 611 | } 612 | .gantt_data_area div, 613 | .gantt_grid div { 614 | -ms-touch-action: none; 615 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 616 | } 617 | .gantt_data_area { 618 | position: relative; 619 | overflow-x: hidden; 620 | overflow-y: hidden; 621 | -moz-user-select: -moz-none; 622 | -webkit-user-select: none; 623 | user-select: none; 624 | } 625 | .gantt_links_area { 626 | position: absolute; 627 | left: 0; 628 | top: 0; 629 | } 630 | .gantt_side_content, 631 | .gantt_task_content, 632 | .gantt_task_progress { 633 | line-height: inherit; 634 | overflow: hidden; 635 | height: 100%; 636 | } 637 | .gantt_task_content { 638 | font-size: 12px; 639 | color: #444444; 640 | overflow: visible; 641 | width: 100%; 642 | top: 0; 643 | cursor: pointer; 644 | position: absolute; 645 | white-space: nowrap; 646 | text-align: center; 647 | } 648 | .gantt_task_progress { 649 | text-align: center; 650 | z-index: 0; 651 | background: #5aa0d3; 652 | background-color: #6fc3ff; 653 | } 654 | .gantt_task_progress_wrapper { 655 | border-radius: inherit; 656 | position: relative; 657 | width: 100%; 658 | height: 100%; 659 | overflow: hidden; 660 | background-color: #9dd3ff; 661 | } 662 | .gantt_task_line { 663 | border-radius: 0; 664 | position: absolute; 665 | box-sizing: border-box; 666 | background-color: #eff6fb; 667 | border: 0px solid #3588c5; 668 | -webkit-user-select: none; 669 | -moz-user-select: none; 670 | -moz-user-select: -moz-none; 671 | } 672 | .gantt_task_line.gantt_drag_move div { 673 | cursor: move; 674 | } 675 | .gantt_touch_move, 676 | .gantt_touch_progress .gantt_touch_resize { 677 | transform: scale(1.02, 1.1); 678 | transform-origin: 50%; 679 | } 680 | .gantt_touch_progress .gantt_task_progress_drag, 681 | .gantt_touch_resize .gantt_task_drag { 682 | transform: scaleY(1.3); 683 | transform-origin: 50%; 684 | } 685 | .gantt_side_content { 686 | position: absolute; 687 | white-space: nowrap; 688 | color: #6e6e6e; 689 | top: 0; 690 | font-size: 11px; 691 | } 692 | .gantt_side_content.gantt_left { 693 | right: 100%; 694 | padding-right: 20px; 695 | } 696 | .gantt_side_content.gantt_right { 697 | left: 100%; 698 | padding-left: 20px; 699 | } 700 | .gantt_side_content.gantt_link_crossing { 701 | bottom: 6.75px; 702 | top: auto; 703 | } 704 | .gantt_link_arrow, 705 | .gantt_task_link .gantt_line_wrapper { 706 | position: absolute; 707 | cursor: pointer; 708 | } 709 | .gantt_line_wrapper div { 710 | background-color: #585858; 711 | } 712 | .gantt_task_link:hover .gantt_line_wrapper div { 713 | box-shadow: 0 0 5px 0 #00e1ff; 714 | } 715 | .gantt_task_link div.gantt_link_arrow { 716 | background-color: transparent; 717 | border-style: solid; 718 | width: 0; 719 | height: 0; 720 | } 721 | .gantt_link_control { 722 | position: absolute; 723 | width: 20px; 724 | top: 0; 725 | } 726 | .gantt_link_control div { 727 | display: none; 728 | cursor: pointer; 729 | box-sizing: border-box; 730 | position: relative; 731 | top: 50%; 732 | margin-top: -7.5px; 733 | vertical-align: middle; 734 | border: 1px solid #929292; 735 | border-radius: 6.5px; 736 | height: 13px; 737 | width: 13px; 738 | background-color: #f0f0f0; 739 | } 740 | .gantt_link_control.task_right div.gantt_link_point { 741 | margin-left: 7px; 742 | } 743 | .gantt_link_control div:hover { 744 | background-color: #fff; 745 | } 746 | .gantt_link_control.task_left { 747 | left: -20px; 748 | } 749 | .gantt_link_control.task_right { 750 | right: -20px; 751 | } 752 | .gantt_link_target .gantt_link_control div, 753 | .gantt_task_line.gantt_drag_move .gantt_link_control div, 754 | .gantt_task_line.gantt_drag_move .gantt_task_drag, 755 | .gantt_task_line.gantt_drag_move .gantt_task_progress_drag, 756 | .gantt_task_line.gantt_drag_progress .gantt_link_control div, 757 | .gantt_task_line.gantt_drag_progress .gantt_task_drag, 758 | .gantt_task_line.gantt_drag_progress .gantt_task_progress_drag, 759 | .gantt_task_line.gantt_drag_resize .gantt_link_control div, 760 | .gantt_task_line.gantt_drag_resize .gantt_task_drag, 761 | .gantt_task_line.gantt_drag_resize .gantt_task_progress_drag, 762 | .gantt_task_line.gantt_selected .gantt_link_control div, 763 | .gantt_task_line.gantt_selected .gantt_task_drag, 764 | .gantt_task_line.gantt_selected .gantt_task_progress_drag, 765 | .gantt_task_line:hover .gantt_link_control div, 766 | .gantt_task_line:hover .gantt_task_drag, 767 | .gantt_task_line:hover .gantt_task_progress_drag { 768 | display: block; 769 | } 770 | .gantt_link_source, 771 | .gantt_link_target { 772 | box-shadow: 0 0 3px #0070fe; 773 | } 774 | .gantt_link_target.link_finish_allow, 775 | .gantt_link_target.link_start_allow { 776 | box-shadow: 0 0 3px #585858; 777 | } 778 | .gantt_link_target.link_finish_deny, 779 | .gantt_link_target.link_start_deny { 780 | box-shadow: 0 0 3px #e87e7b; 781 | } 782 | .link_finish_allow .gantt_link_control.task_end_date div, 783 | .link_start_allow .gantt_link_control.task_start_date div { 784 | background-color: #585858; 785 | border-color: #585858; 786 | } 787 | .link_finish_deny .gantt_link_control.task_end_date div, 788 | .link_start_deny .gantt_link_control.task_start_date div { 789 | background-color: #e87e7b; 790 | border-color: #dd3e3a; 791 | } 792 | .gantt_link_arrow_right { 793 | border-width: 4px 0 4px 8px; 794 | border-top-color: transparent !important; 795 | border-right-color: transparent !important; 796 | border-bottom-color: transparent !important; 797 | border-left-color: #585858; 798 | margin-top: -1px; 799 | } 800 | .gantt_link_arrow_left { 801 | border-width: 4px 8px 4px 0; 802 | margin-top: -1px; 803 | border-top-color: transparent !important; 804 | border-right-color: #585858; 805 | border-bottom-color: transparent !important; 806 | border-left-color: transparent !important; 807 | } 808 | .gantt_link_arrow_up { 809 | border-width: 0 4px 8px; 810 | border-color: transparent transparent #585858; 811 | border-top-color: transparent !important; 812 | border-right-color: transparent !important; 813 | border-bottom-color: #585858; 814 | border-left-color: transparent !important; 815 | } 816 | .gantt_link_arrow_down { 817 | border-width: 4px 8px 0 4px; 818 | border-top-color: #585858; 819 | border-right-color: transparent !important; 820 | border-bottom-color: transparent !important; 821 | border-left-color: transparent !important; 822 | } 823 | .gantt_task_drag, 824 | .gantt_task_progress_drag { 825 | cursor: ew-resize; 826 | display: none; 827 | position: absolute; 828 | } 829 | .gantt_task_drag.task_right { 830 | cursor: e-resize; 831 | } 832 | .gantt_task_drag.task_left { 833 | cursor: w-resize; 834 | } 835 | .gantt_task_drag { 836 | height: 100%; 837 | width: 8px; 838 | z-index: 1; 839 | top: -1px; 840 | } 841 | .gantt_task_drag.task_left { 842 | left: -7px; 843 | } 844 | .gantt_task_drag.task_right { 845 | right: -7px; 846 | } 847 | .gantt_task_progress_drag { 848 | height: 8px; 849 | width: 8px; 850 | bottom: -4px; 851 | margin-left: -4px; 852 | background-position: bottom; 853 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDNGNzQ1MTZCQkRBMTFFMjlBMjRBRkU0RkNCMTUzNkUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDNGNzQ1MTdCQkRBMTFFMjlBMjRBRkU0RkNCMTUzNkUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0M0Y3NDUxNEJCREExMUUyOUEyNEFGRTRGQ0IxNTM2RSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0M0Y3NDUxNUJCREExMUUyOUEyNEFGRTRGQ0IxNTM2RSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrBLI3EAAACISURBVHjafE07DkMhDAuftQdgYuCWbZ9a0WtwGJC4BgMbYmVGaZLpqWpryZFjOwrACSmlSLzCN1DwrLUik/TtM3yUUnDOiWMMZE3enTNF4gghRO89KKXkABGh9w6ttcPSHp1zsNaCvbcUjDHAHhVemg1rrVxprYWs2ZOMR84ZfoGfXuAP3gIMABorQUvC1invAAAAAElFTkSuQmCC); 854 | background-repeat: no-repeat; 855 | z-index: 1; 856 | } 857 | .gantt_task_progress_drag:hover { 858 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAUklEQVQY04XNsQ2AMAxE0W+oGYjZCAIxmuk9gze5VCmIIvLaf9KtfD3ADrwM3JmpzBRw9PGKCDURIaC0eLq7eu4uoBggSaNLzIyFCQM0G2x/gwpRwTYDaW9lwQAAAABJRU5ErkJggg==); 859 | } 860 | .gantt_link_tooltip { 861 | box-shadow: 3px 3px 3px #888; 862 | background-color: #fff; 863 | border-left: 1px dotted #cecece; 864 | border-top: 1px dotted #cecece; 865 | font-family: Tahoma; 866 | font-size: 8pt; 867 | color: #444; 868 | padding: 6px; 869 | line-height: 20px; 870 | } 871 | .gantt_link_direction { 872 | height: 0; 873 | border: 0 none #585858; 874 | border-bottom-style: dashed; 875 | border-bottom-width: 2px; 876 | transform-origin: 0 0; 877 | -ms-transform-origin: 0 0; 878 | -webkit-transform-origin: 0 0; 879 | z-index: 2; 880 | margin-left: 1px; 881 | position: absolute; 882 | } 883 | .gantt_grid_data .gantt_row.gantt_selected, 884 | .gantt_grid_data .gantt_row.odd.gantt_selected, 885 | .gantt_task_row.gantt_selected { 886 | background-color: #ffe6b1 !important; 887 | border-bottom-color: #ffc341; 888 | } 889 | .gantt_task_row.gantt_selected { 890 | background-color: #ffebc1; 891 | background-image: linear-gradient(0deg, #ffe09d 0, #ffeabb); 892 | border-top-color: #ffc341; 893 | } 894 | .gantt_task_row.gantt_selected .gantt_task_cell { 895 | border-right-color: #ffb30e; 896 | border-right-color: #ffce65; 897 | } 898 | .gantt_task_line.gantt_selected { 899 | box-shadow: 0 0 5px #5aa0d3; 900 | } 901 | .gantt_task_line.gantt_project.gantt_selected { 902 | box-shadow: 0 0 5px #9ab9f1; 903 | } 904 | .gantt_task_line.gantt_milestone { 905 | visibility: hidden; 906 | background-color: #db7dc5; 907 | border: 0 solid #cd49ae; 908 | box-sizing: content-box; 909 | -moz-box-sizing: content-box; 910 | } 911 | .gantt_task_line.gantt_milestone div { 912 | visibility: visible; 913 | } 914 | .gantt_task_line.gantt_milestone .gantt_task_content { 915 | background: inherit; 916 | border: inherit; 917 | border-width: 1px; 918 | border-radius: inherit; 919 | box-sizing: border-box; 920 | -moz-box-sizing: border-box; 921 | transform: rotate(45deg); 922 | } 923 | .gantt_task_line.gantt_task_inline_color { 924 | border-color: #999; 925 | } 926 | .gantt_task_line.gantt_task_inline_color .gantt_task_progress { 927 | background-color: #363636; 928 | opacity: 0.2; 929 | } 930 | .gantt_task_line.gantt_task_inline_color.gantt_project.gantt_selected, 931 | .gantt_task_line.gantt_task_inline_color.gantt_selected { 932 | box-shadow: 0 0 5px #999; 933 | } 934 | .gantt_task_link.gantt_link_inline_color:hover .gantt_line_wrapper div { 935 | box-shadow: 0 0 5px 0 #999; 936 | } 937 | .gantt_critical_task { 938 | background-color: #e63030; 939 | border-color: #9d3a3a; 940 | } 941 | .gantt_critical_task .gantt_task_progress { 942 | background-color: rgba(0, 0, 0, 0.4); 943 | } 944 | .gantt_critical_link .gantt_line_wrapper > div { 945 | background-color: #e63030; 946 | } 947 | .gantt_critical_link .gantt_link_arrow { 948 | border-color: #e63030; 949 | } 950 | .gantt_btn_set:focus, 951 | .gantt_cell:focus, 952 | .gantt_grid_head_cell:focus, 953 | .gantt_popup_button:focus, 954 | .gantt_qi_big_icon:focus, 955 | .gantt_row:focus { 956 | box-shadow: inset 0 0 1px 1px #4d90fe; 957 | } 958 | .gantt_split_parent, 959 | .gantt_split_subproject { 960 | opacity: 0.1; 961 | pointer-events: none; 962 | } 963 | .gantt_unselectable, 964 | .gantt_unselectable div { 965 | -webkit-user-select: none; 966 | -moz-user-select: none; 967 | -moz-user-select: -moz-none; 968 | } 969 | .gantt_cal_light { 970 | -webkit-tap-highlight-color: transparent; 971 | background-color: #eff6fb; 972 | border-radius: 0; 973 | font-family: Tahoma; 974 | font-size: 11px; 975 | border: 1px solid #a4bed4; 976 | color: #42464b; 977 | position: absolute; 978 | z-index: 10001; 979 | width: 550px; 980 | height: 250px; 981 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.07); 982 | } 983 | .gantt_cal_light_wide { 984 | width: 650px; 985 | } 986 | .gantt_cal_light select { 987 | font-family: Tahoma; 988 | border: 1px solid #a4bed4; 989 | font-size: 11px; 990 | padding: 2px; 991 | margin: 0; 992 | } 993 | .gantt_cal_ltitle { 994 | padding: 7px 10px; 995 | overflow: hidden; 996 | -webkit-border-top-left-radius: 0; 997 | -webkit-border-bottom-left-radius: 0; 998 | -webkit-border-top-right-radius: 0; 999 | -webkit-border-bottom-right-radius: 0; 1000 | -moz-border-radius-topleft: 0; 1001 | -moz-border-radius-bottomleft: 0; 1002 | -moz-border-radius-topright: 0; 1003 | -moz-border-radius-bottomright: 0; 1004 | border-top-left-radius: 0; 1005 | border-bottom-left-radius: 0; 1006 | border-top-right-radius: 0; 1007 | border-bottom-right-radius: 0; 1008 | } 1009 | .gantt_cal_ltitle, 1010 | .gantt_cal_ltitle span { 1011 | white-space: nowrap; 1012 | } 1013 | .gantt_cal_lsection { 1014 | color: #727272; 1015 | font-weight: 700; 1016 | padding: 12px 0 5px 10px; 1017 | } 1018 | .gantt_cal_lsection .gantt_fullday { 1019 | float: right; 1020 | margin-right: 5px; 1021 | font-size: 12px; 1022 | font-weight: 400; 1023 | line-height: 20px; 1024 | vertical-align: top; 1025 | cursor: pointer; 1026 | } 1027 | .gantt_cal_lsection { 1028 | font-size: 13px; 1029 | } 1030 | .gantt_cal_ltext { 1031 | padding: 2px 10px; 1032 | overflow: hidden; 1033 | } 1034 | .gantt_cal_ltext textarea { 1035 | overflow-y: auto; 1036 | overflow-x: hidden; 1037 | font-family: Tahoma; 1038 | font-size: 11px; 1039 | box-sizing: border-box; 1040 | border: 1px solid #a4bed4; 1041 | height: 100%; 1042 | width: 100%; 1043 | outline: none !important; 1044 | resize: none; 1045 | } 1046 | .gantt_section_constraint [data-constraint-time-select] { 1047 | margin-left: 20px; 1048 | } 1049 | .gantt_time { 1050 | font-weight: 700; 1051 | } 1052 | .gantt_cal_light .gantt_title { 1053 | padding-left: 10px; 1054 | } 1055 | .gantt_cal_larea { 1056 | border: 1px solid #a4bed4; 1057 | border-left: none; 1058 | border-right: none; 1059 | background-color: #fff; 1060 | overflow: hidden; 1061 | height: 1px; 1062 | } 1063 | .gantt_btn_set { 1064 | margin: 10px 7px 5px 10px; 1065 | padding: 2px 25px 2px 10px; 1066 | float: left; 1067 | border-radius: 4px; 1068 | border: 1px solid #a4bed4; 1069 | height: 26px; 1070 | color: #42464b; 1071 | background: #fff; 1072 | background-color: #f8f8f8; 1073 | background-image: linear-gradient(0deg, #e6e6e6 0, #fff); 1074 | box-sizing: border-box; 1075 | cursor: pointer; 1076 | } 1077 | .gantt_hidden { 1078 | display: none; 1079 | } 1080 | .gantt_btn_set div { 1081 | float: left; 1082 | font-size: 13px; 1083 | height: 20px; 1084 | line-height: 20px; 1085 | background-repeat: no-repeat; 1086 | vertical-align: middle; 1087 | } 1088 | .gantt_save_btn { 1089 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QkE5Nzc4RENDMzAzMTFFMjk4QjNBODhDMUM4QUUwNEQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QkE5Nzc4RERDMzAzMTFFMjk4QjNBODhDMUM4QUUwNEQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCQTk3NzhEQUMzMDMxMUUyOThCM0E4OEMxQzhBRTA0RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCQTk3NzhEQkMzMDMxMUUyOThCM0E4OEMxQzhBRTA0RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pr5Ot2UAAAFoSURBVHjapFO7SgNRED0rxkjEELbcQksLBUMg4EfkG7YV/AFTpBAUfNQisVDQD7CQlBaWwoZ8gNEiFttmg3GTfbF6JtzFTUJYyIHhDnfOnJm5Dy2OYywDjQJnF5ex/dXDweENNtaAzT/jur46IY1D4McHvv3J+nZ7BGNrG436sSaU3ucHqtUqzP1sVcflMizLEl8EwjBEFEXwomwC5DInEeDGaDSC62cTIJc5iUAQBGJukE1A8YkVNYLv++h232WMRUYOuakRlOLTwzU8z1tYPZ/Po1QqJR0kAgQDWTEjwLaIWq0GwzDmJtm2jVarNSvAeeQQXVe6ME1Tgs1mMyXA2GAwQKFQmH8G/X4fjuOgWCxKkP40yMnlcukO1MNgAivoui5B+tMgh3H1DuQa66fnaLfbGA6HMgY7oNGfNnL+v0RN/cbnl9f46qSBSqUiM9J4ZQSvVgl0Oh1pf2d3D4/3d5q27Hf+FWAAc90EKSR5k78AAAAASUVORK5CYII=); 1090 | margin-top: 2px; 1091 | width: 21px; 1092 | } 1093 | .gantt_cancel_btn { 1094 | margin-top: 2px; 1095 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QzU4NTQ3NUZDMzAzMTFFMkE0MjRGNTQzQjE0MTNDQkIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QzU4NTQ3NjBDMzAzMTFFMkE0MjRGNTQzQjE0MTNDQkIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDNTg1NDc1REMzMDMxMUUyQTQyNEY1NDNCMTQxM0NCQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpDNTg1NDc1RUMzMDMxMUUyQTQyNEY1NDNCMTQxM0NCQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkVB3hoAAAEYSURBVHjapJM9CsJAEIUTFStbQRA8gKCNKSIIeohUVraC9oKiGBRsBDvBylbwEkJQCys9gJUgeIOAxDfwEpYNpsmDL7PZ+cn+TMwgCIw0yhgplZPH3bbVuRqYgiYogxe4ABc8wqD69RpbQR6MwQ04TDZoHc6LP/tvC2uw4Fi+Vgcmrct58W9iW4BaYAB80OGSQ8my7xz7jDsAT11Bn3alJYvUa1pp8VGBNu0uIVm2s9fiowJF8OWJ/0sWPRlX1At8eLqlhGRRhXEfvcCJtpeQLOpq8VGBLe04Ibmh+Ld6AY8HWOBVzUCVvio780z/LrxCtQ9EQ+5tBOZElRzeUmmqWCfKlyfAAkfw5vyb7xb9vlrATPs7/wQYAISgQGDaq6hUAAAAAElFTkSuQmCC); 1096 | width: 20px; 1097 | } 1098 | .gantt_delete_btn { 1099 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RTA3M0M1MzJDMzAzMTFFMkE5ODZDRjhENzQ2MUZFNzkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RTA3M0M1MzNDMzAzMTFFMkE5ODZDRjhENzQ2MUZFNzkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFMDczQzUzMEMzMDMxMUUyQTk4NkNGOEQ3NDYxRkU3OSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFMDczQzUzMUMzMDMxMUUyQTk4NkNGOEQ3NDYxRkU3OSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pplu0sEAAAErSURBVHja3FOxboMwED0nIQNbJoaOZM0YyMhGqwz8BOQ3mm5I/AHwCayoGVlhZUSMHTIhsSCEhOtzawRIlZDY+qTTs893z6fzmVBKYQ12YhEEweB0HAf3tCxLUFUVXWSeiDGIzW/ynRE9HA7coihCBtd1AVn40TCO2X1ewbthGCBJEiiKAtvtFggh0HUdWJYFfd9zez6f3JckiS1EhEDmeZ623+9BlmWejCaAfWqahou0bQumab7MK9DP5zM9nU5c4Hg8ch4nF0XBOc9zuF6vg/pm3pw0TSdNDcPwp8QsG2LiOIY/BZagqqp1AmP8M4Gvuq5B1/XJqNq2zVnTNMBzjBsLEHxnHBrf91/Z/nPBpW+32+0hPuFODAt79wtbfiwQuLD4x6SCNfgWYAAfQYJFsOV+5AAAAABJRU5ErkJggg==); 1100 | margin-top: 2px; 1101 | width: 20px; 1102 | } 1103 | .gantt_cal_cover { 1104 | width: 100%; 1105 | height: 100%; 1106 | position: fixed; 1107 | z-index: 10000; 1108 | top: 0; 1109 | left: 0; 1110 | background-color: #000; 1111 | opacity: 0.1; 1112 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=10); 1113 | } 1114 | .gantt_custom_button { 1115 | padding: 0 3px; 1116 | font-family: Tahoma; 1117 | font-size: 11px; 1118 | font-weight: 400; 1119 | margin-right: 10px; 1120 | margin-top: -5px; 1121 | cursor: pointer; 1122 | float: right; 1123 | height: 21px; 1124 | width: 90px; 1125 | border: 1px solid #cecece; 1126 | text-align: center; 1127 | border-radius: 4px; 1128 | } 1129 | .gantt_custom_button div { 1130 | cursor: pointer; 1131 | float: none; 1132 | height: 21px; 1133 | line-height: 21px; 1134 | vertical-align: middle; 1135 | } 1136 | .gantt_custom_button div:first-child { 1137 | display: none; 1138 | } 1139 | .gantt_cal_light_wide { 1140 | width: 580px; 1141 | padding: 2px 4px; 1142 | } 1143 | .gantt_cal_light_wide .gantt_cal_larea { 1144 | box-sizing: border-box; 1145 | border: 1px solid #a4bed4; 1146 | } 1147 | .gantt_cal_light_wide .gantt_cal_lsection { 1148 | border: 0; 1149 | float: left; 1150 | text-align: right; 1151 | width: 80px; 1152 | height: 20px; 1153 | padding: 5px 10px 0 0; 1154 | } 1155 | .gantt_cal_light_wide .gantt_wrap_section { 1156 | position: relative; 1157 | padding: 10px 0; 1158 | overflow: hidden; 1159 | border-bottom: 1px solid #ebebeb; 1160 | } 1161 | .gantt_cal_light_wide .gantt_section_time { 1162 | overflow: hidden; 1163 | padding-top: 2px !important; 1164 | padding-right: 0; 1165 | height: 20px !important; 1166 | } 1167 | .gantt_cal_light_wide .gantt_cal_ltext { 1168 | padding-right: 0; 1169 | } 1170 | .gantt_cal_light_wide .gantt_cal_larea { 1171 | padding: 0 10px; 1172 | width: 100%; 1173 | } 1174 | .gantt_cal_light_wide .gantt_section_time { 1175 | background: transparent; 1176 | } 1177 | .gantt_cal_light_wide .gantt_cal_checkbox label { 1178 | padding-left: 0; 1179 | } 1180 | .gantt_cal_light_wide .gantt_cal_lsection .gantt_fullday { 1181 | float: none; 1182 | margin-right: 0; 1183 | font-weight: 700; 1184 | cursor: pointer; 1185 | } 1186 | .gantt_cal_light_wide .gantt_custom_button { 1187 | position: absolute; 1188 | top: 0; 1189 | right: 0; 1190 | margin-top: 2px; 1191 | } 1192 | .gantt_cal_light_wide .gantt_repeat_right { 1193 | margin-right: 55px; 1194 | } 1195 | .gantt_cal_light_wide.gantt_cal_light_full { 1196 | width: 738px; 1197 | } 1198 | .gantt_cal_wide_checkbox input { 1199 | margin-top: 8px; 1200 | margin-left: 14px; 1201 | } 1202 | .gantt_cal_light input { 1203 | font-size: 11px; 1204 | } 1205 | .gantt_section_time { 1206 | background-color: #fff; 1207 | white-space: nowrap; 1208 | padding: 2px 10px 5px; 1209 | padding-top: 2px !important; 1210 | } 1211 | .gantt_section_time .gantt_time_selects { 1212 | float: left; 1213 | height: 25px; 1214 | } 1215 | .gantt_section_time .gantt_time_selects select { 1216 | height: 23px; 1217 | padding: 2px; 1218 | border: 1px solid #a4bed4; 1219 | } 1220 | .gantt_duration { 1221 | width: 100px; 1222 | height: 23px; 1223 | float: left; 1224 | white-space: nowrap; 1225 | margin-left: 20px; 1226 | line-height: 23px; 1227 | } 1228 | .gantt_duration .gantt_duration_dec, 1229 | .gantt_duration .gantt_duration_inc, 1230 | .gantt_duration .gantt_duration_value { 1231 | box-sizing: border-box; 1232 | text-align: center; 1233 | vertical-align: top; 1234 | height: 100%; 1235 | border: 1px solid #a4bed4; 1236 | } 1237 | .gantt_duration .gantt_duration_value { 1238 | width: 40px; 1239 | padding: 3px 4px; 1240 | border-left-width: 0; 1241 | border-right-width: 0; 1242 | } 1243 | .gantt_duration .gantt_duration_value.gantt_duration_value_formatted { 1244 | width: 70px; 1245 | } 1246 | .gantt_duration .gantt_duration_dec, 1247 | .gantt_duration .gantt_duration_inc { 1248 | width: 20px; 1249 | padding: 1px; 1250 | padding-bottom: 1px; 1251 | background: #fff; 1252 | background-color: #f8f8f8; 1253 | background-image: linear-gradient(0deg, #e6e6e6 0, #fff); 1254 | } 1255 | .gantt_duration .gantt_duration_dec { 1256 | -moz-border-top-left-radius: 4px; 1257 | -moz-border-bottom-left-radius: 4px; 1258 | -webkit-border-top-left-radius: 4px; 1259 | -webkit-border-bottom-left-radius: 4px; 1260 | border-top-left-radius: 4px; 1261 | border-bottom-left-radius: 4px; 1262 | } 1263 | .gantt_duration .gantt_duration_inc { 1264 | margin-right: 4px; 1265 | -moz-border-top-right-radius: 4px; 1266 | -moz-border-bottom-right-radius: 4px; 1267 | -webkit-border-top-right-radius: 4px; 1268 | -webkit-border-bottom-right-radius: 4px; 1269 | border-top-right-radius: 4px; 1270 | border-bottom-right-radius: 4px; 1271 | } 1272 | .gantt_resources { 1273 | max-height: 150px; 1274 | height: auto; 1275 | overflow-y: auto; 1276 | } 1277 | .gantt_resource_row { 1278 | display: block; 1279 | padding: 10px 0; 1280 | border-bottom: 1px solid #ebebeb; 1281 | cursor: pointer; 1282 | } 1283 | .gantt_resource_row input[type='checkbox']:not(:checked), 1284 | .gantt_resource_row input[type='checkbox']:not(:checked) ~ div { 1285 | opacity: 0.5; 1286 | } 1287 | .gantt_resource_toggle { 1288 | vertical-align: middle; 1289 | } 1290 | .gantt_resources_filter .gantt_resources_filter_input { 1291 | padding: 1px 2px; 1292 | box-sizing: border-box; 1293 | } 1294 | .gantt_resources_filter .switch_unsetted { 1295 | vertical-align: middle; 1296 | } 1297 | .gantt_resource_cell { 1298 | display: inline-block; 1299 | } 1300 | .gantt_resource_cell.gantt_resource_cell_checkbox { 1301 | width: 24px; 1302 | max-width: 24px; 1303 | min-width: 24px; 1304 | vertical-align: middle; 1305 | } 1306 | .gantt_resource_cell.gantt_resource_cell_label { 1307 | width: 40%; 1308 | max-width: 40%; 1309 | vertical-align: middle; 1310 | } 1311 | .gantt_resource_cell.gantt_resource_cell_value { 1312 | width: 30%; 1313 | max-width: 30%; 1314 | vertical-align: middle; 1315 | } 1316 | .gantt_resource_cell.gantt_resource_cell_value input, 1317 | .gantt_resource_cell.gantt_resource_cell_value select { 1318 | width: 80%; 1319 | vertical-align: middle; 1320 | padding: 1px 2px; 1321 | box-sizing: border-box; 1322 | } 1323 | .gantt_resource_cell.gantt_resource_cell_unit { 1324 | width: 10%; 1325 | max-width: 10%; 1326 | vertical-align: middle; 1327 | } 1328 | .gantt_resource_early_value { 1329 | opacity: 0.8; 1330 | font-size: 0.9em; 1331 | } 1332 | .gantt_cal_quick_info { 1333 | border: 1px solid #a4bed4; 1334 | border-radius: 0; 1335 | position: absolute; 1336 | z-index: 300; 1337 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.07); 1338 | background-color: rgb(255, 255, 255); 1339 | width: auto; 1340 | transition: left 0.5s ease, right 0.5s; 1341 | -moz-transition: left 0.5s ease, right 0.5s; 1342 | -webkit-transition: left 0.5s ease, right 0.5s; 1343 | -o-transition: left 0.5s ease, right 0.5s; 1344 | } 1345 | .gantt_no_animate { 1346 | transition: none; 1347 | -moz-transition: none; 1348 | -webkit-transition: none; 1349 | -o-transition: none; 1350 | } 1351 | .gantt_cal_quick_info.gantt_qi_left .gantt_qi_big_icon { 1352 | float: none; 1353 | } 1354 | .gantt_cal_qi_title { 1355 | -webkit-border-top-left-radius: 0; 1356 | -webkit-border-bottom-left-radius: 0; 1357 | -webkit-border-top-right-radius: 0; 1358 | -webkit-border-bottom-right-radius: 0; 1359 | -moz-border-radius-topleft: 0; 1360 | -moz-border-radius-bottomleft: 0; 1361 | -moz-border-radius-topright: 0; 1362 | -moz-border-radius-bottomright: 0; 1363 | border-top-left-radius: 0; 1364 | border-bottom-left-radius: 0; 1365 | border-top-right-radius: 0; 1366 | border-bottom-right-radius: 0; 1367 | padding: 5px 0 8px 12px; 1368 | color: #1e2022; 1369 | box-shadow: inset 0 1px 1px #fff; 1370 | background-color: #dfedff; 1371 | background: -webkit-gradient( 1372 | linear, 1373 | left top, 1374 | left bottom, 1375 | color-stop(0, #e4f0ff), 1376 | color-stop(50%, #dfedff), 1377 | color-stop(100%, #d5e8ff) 1378 | ); 1379 | background-image: -o-linear-gradient( 1380 | top, 1381 | #e4f0ff 0, 1382 | #dfedff 60%, 1383 | #d5e8ff 100% 1384 | ); 1385 | background-position: 0 1px; 1386 | background-repeat: repeat-x; 1387 | border-bottom: 1px solid #a4bed4; 1388 | } 1389 | .gantt_cal_qi_tdate { 1390 | font-size: 14px; 1391 | font-weight: 700; 1392 | } 1393 | .gantt_cal_qi_tcontent { 1394 | font-size: 11px; 1395 | } 1396 | .gantt_cal_qi_content { 1397 | padding: 16px 8px; 1398 | font-size: 13px; 1399 | color: #1e2022; 1400 | overflow: hidden; 1401 | } 1402 | .gantt_cal_qi_controls { 1403 | -webkit-border-top-left-radius: 0; 1404 | -webkit-border-bottom-left-radius: 0; 1405 | -webkit-border-top-right-radius: 0; 1406 | -webkit-border-bottom-right-radius: 0; 1407 | -moz-border-radius-topleft: 0; 1408 | -moz-border-radius-bottomleft: 0; 1409 | -moz-border-radius-topright: 0; 1410 | -moz-border-radius-bottomright: 0; 1411 | border-top-left-radius: 0; 1412 | border-bottom-left-radius: 0; 1413 | border-top-right-radius: 0; 1414 | border-bottom-right-radius: 0; 1415 | padding-left: 7px; 1416 | } 1417 | .gantt_cal_qi_controls .gantt_menu_icon { 1418 | margin-top: 3px; 1419 | background-repeat: no-repeat; 1420 | } 1421 | .gantt_cal_qi_controls .gantt_menu_icon.icon_edit { 1422 | width: 20px; 1423 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3QYFDhEuX3ujFgAAAFNJREFUOMvt0zEOACAIA0DksTyqn8XJTRTKZGJXyaWEKPKTCQAH4Ls37cItcDUzsxHNDLZNhCq7Gt1wh9ErV7EjyGAhyGLphlnsClWuS32rn0czAV+sNUIROnQoAAAAAElFTkSuQmCC); 1424 | } 1425 | .gantt_cal_qi_controls .gantt_menu_icon.icon_delete { 1426 | width: 20px; 1427 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RTA3M0M1MzJDMzAzMTFFMkE5ODZDRjhENzQ2MUZFNzkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RTA3M0M1MzNDMzAzMTFFMkE5ODZDRjhENzQ2MUZFNzkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFMDczQzUzMEMzMDMxMUUyQTk4NkNGOEQ3NDYxRkU3OSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFMDczQzUzMUMzMDMxMUUyQTk4NkNGOEQ3NDYxRkU3OSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pplu0sEAAAErSURBVHja3FOxboMwED0nIQNbJoaOZM0YyMhGqwz8BOQ3mm5I/AHwCayoGVlhZUSMHTIhsSCEhOtzawRIlZDY+qTTs893z6fzmVBKYQ12YhEEweB0HAf3tCxLUFUVXWSeiDGIzW/ynRE9HA7coihCBtd1AVn40TCO2X1ewbthGCBJEiiKAtvtFggh0HUdWJYFfd9zez6f3JckiS1EhEDmeZ623+9BlmWejCaAfWqahou0bQumab7MK9DP5zM9nU5c4Hg8ch4nF0XBOc9zuF6vg/pm3pw0TSdNDcPwp8QsG2LiOIY/BZagqqp1AmP8M4Gvuq5B1/XJqNq2zVnTNMBzjBsLEHxnHBrf91/Z/nPBpW+32+0hPuFODAt79wtbfiwQuLD4x6SCNfgWYAAfQYJFsOV+5AAAAABJRU5ErkJggg==); 1428 | } 1429 | .gantt_qi_big_icon { 1430 | font-size: 13px; 1431 | border-radius: 4px; 1432 | color: #42464b; 1433 | background: #fff; 1434 | background-color: #f8f8f8; 1435 | background-image: linear-gradient(0deg, #e6e6e6 0, #fff); 1436 | margin: 5px 9px 8px 0; 1437 | min-width: 60px; 1438 | line-height: 26px; 1439 | vertical-align: middle; 1440 | padding: 0 10px 0 5px; 1441 | cursor: pointer; 1442 | border: 1px solid #a4bed4; 1443 | } 1444 | .gantt_cal_qi_controls div { 1445 | float: left; 1446 | height: 26px; 1447 | text-align: center; 1448 | line-height: 26px; 1449 | } 1450 | .gantt_tooltip { 1451 | padding: 10px; 1452 | position: absolute; 1453 | z-index: 50; 1454 | white-space: nowrap; 1455 | } 1456 | .gantt_resource_marker { 1457 | position: absolute; 1458 | text-align: center; 1459 | font-size: 14px; 1460 | color: #fff; 1461 | } 1462 | .gantt_resource_marker_ok { 1463 | background: rgba(78, 208, 134, 0.75); 1464 | } 1465 | .gantt_resource_marker_overtime { 1466 | background: hsla(0, 100%, 76%, 0.69); 1467 | } 1468 | .gantt_histogram_label { 1469 | width: 100%; 1470 | height: 100%; 1471 | position: absolute; 1472 | z-index: 1; 1473 | font-weight: 700; 1474 | font-size: 11px; 1475 | } 1476 | .gantt_histogram_fill { 1477 | background-color: rgba(41, 157, 180, 0.2); 1478 | width: 100%; 1479 | position: absolute; 1480 | bottom: 0; 1481 | } 1482 | .gantt_histogram_hor_bar { 1483 | height: 1px; 1484 | margin-top: -1px; 1485 | } 1486 | .gantt_histogram_hor_bar, 1487 | .gantt_histogram_vert_bar { 1488 | position: absolute; 1489 | background: #299db4; 1490 | margin-left: -1px; 1491 | } 1492 | .gantt_histogram_vert_bar { 1493 | width: 1px; 1494 | } 1495 | .gantt_histogram_cell { 1496 | position: absolute; 1497 | text-align: center; 1498 | font-size: 11px; 1499 | color: #000; 1500 | } 1501 | .gantt_marker { 1502 | height: 100%; 1503 | width: 2px; 1504 | top: 0; 1505 | position: absolute; 1506 | text-align: center; 1507 | background-color: rgba(255, 0, 0, 0.4); 1508 | box-sizing: border-box; 1509 | } 1510 | .gantt_marker .gantt_marker_content { 1511 | padding: 5px; 1512 | background: inherit; 1513 | color: #fff; 1514 | position: absolute; 1515 | font-size: 12px; 1516 | line-height: 12px; 1517 | opacity: 0.8; 1518 | } 1519 | .gantt_marker_area { 1520 | position: absolute; 1521 | top: 0; 1522 | left: 0; 1523 | } 1524 | .gantt_grid_editor_placeholder { 1525 | position: absolute; 1526 | } 1527 | .gantt_grid_editor_placeholder > div, 1528 | .gantt_grid_editor_placeholder input, 1529 | .gantt_grid_editor_placeholder select { 1530 | width: 100%; 1531 | height: 100%; 1532 | box-sizing: border-box; 1533 | } 1534 | .gantt_row_placeholder div { 1535 | opacity: 0.5; 1536 | } 1537 | .gantt_row_placeholder .gantt_add, 1538 | .gantt_row_placeholder .gantt_file { 1539 | display: none; 1540 | } 1541 | .gantt_drag_marker.gantt_grid_dnd_marker { 1542 | background-color: transparent; 1543 | transition: all 0.1s ease; 1544 | } 1545 | .gantt_grid_dnd_marker_line { 1546 | height: 4px; 1547 | width: 100%; 1548 | background-color: #3498db; 1549 | } 1550 | .gantt_grid_dnd_marker_line:before { 1551 | background: #fff; 1552 | width: 12px; 1553 | height: 12px; 1554 | box-sizing: border-box; 1555 | border: 3px solid #3498db; 1556 | border-radius: 6px; 1557 | content: ''; 1558 | line-height: 1px; 1559 | display: block; 1560 | position: absolute; 1561 | margin-left: -11px; 1562 | margin-top: -4px; 1563 | pointer-events: none; 1564 | } 1565 | .gantt_grid_dnd_marker_folder { 1566 | height: 100%; 1567 | width: 100%; 1568 | position: absolute; 1569 | pointer-events: none; 1570 | box-sizing: border-box; 1571 | box-shadow: inset 0 0 0 2px #3f98db; 1572 | background: transparent; 1573 | } 1574 | .gantt_overlay_area { 1575 | display: none; 1576 | } 1577 | .gantt_overlay, 1578 | .gantt_overlay_area { 1579 | position: absolute; 1580 | height: inherit; 1581 | width: inherit; 1582 | top: 0; 1583 | left: 0; 1584 | } 1585 | .gantt_click_drag_rect { 1586 | position: absolute; 1587 | left: 0; 1588 | top: 0; 1589 | outline: 1px solid #3f98db; 1590 | background-color: rgba(52, 152, 219, 0.3); 1591 | } 1592 | .gantt_timeline_move_available, 1593 | .gantt_timeline_move_available * { 1594 | cursor: move; 1595 | } 1596 | .gantt_rtl .gantt_grid { 1597 | text-align: right; 1598 | } 1599 | .gantt_rtl .gantt_cell, 1600 | .gantt_rtl .gantt_row { 1601 | flex-direction: row-reverse; 1602 | } 1603 | .gantt_layout_content { 1604 | width: 100%; 1605 | overflow: auto; 1606 | box-sizing: border-box; 1607 | } 1608 | .gantt_layout_cell { 1609 | position: relative; 1610 | box-sizing: border-box; 1611 | } 1612 | .gantt_layout_cell > .gantt_layout_header { 1613 | background: #33aae8; 1614 | color: #fff; 1615 | font-size: 17px; 1616 | padding: 5px 10px; 1617 | box-sizing: border-box; 1618 | } 1619 | .gantt_layout_header.collapsed_x { 1620 | background: #a9a9a9; 1621 | } 1622 | .gantt_layout_header.collapsed_x .gantt_header_arrow:before { 1623 | content: '\21E7'; 1624 | } 1625 | .gantt_layout_header.collapsed_y { 1626 | background: #a9a9a9; 1627 | } 1628 | .gantt_layout_header.collapsed_y .gantt_header_arrow:before { 1629 | content: '\21E9'; 1630 | } 1631 | .gantt_layout_header { 1632 | cursor: pointer; 1633 | } 1634 | .gantt_layout_header .gantt_header_arrow { 1635 | float: right; 1636 | text-align: right; 1637 | } 1638 | .gantt_layout_header .gantt_header_arrow:before { 1639 | content: '\21E6'; 1640 | } 1641 | .gantt_layout_header.vertical .gantt_header_arrow:before { 1642 | content: '\21E7'; 1643 | } 1644 | .gantt_layout_outer_scroll_vertical .gantt_layout_content { 1645 | overflow-y: hidden; 1646 | } 1647 | .gantt_layout_outer_scroll_horizontal .gantt_layout_content { 1648 | overflow-x: hidden; 1649 | } 1650 | .gantt_layout_x > .gantt_layout_cell { 1651 | display: inline-block; 1652 | vertical-align: top; 1653 | } 1654 | .gantt_layout_x { 1655 | white-space: nowrap; 1656 | } 1657 | .gantt_resizing { 1658 | opacity: 0.7; 1659 | background: #f2f2f2; 1660 | } 1661 | .gantt_layout_cell_border_right.gantt_resizer { 1662 | overflow: visible; 1663 | border-right: 0; 1664 | } 1665 | .gantt_resizer { 1666 | cursor: e-resize; 1667 | position: relative; 1668 | } 1669 | .gantt_resizer_y { 1670 | cursor: n-resize; 1671 | } 1672 | .gantt_resizer_stick { 1673 | background: #33aae8; 1674 | z-index: 9999; 1675 | position: absolute; 1676 | top: 0; 1677 | width: 100%; 1678 | } 1679 | .gantt_resizer_x .gantt_resizer_x { 1680 | position: absolute; 1681 | width: 20px; 1682 | height: 100%; 1683 | margin-left: -10px; 1684 | top: 0; 1685 | left: 0; 1686 | z-index: 1; 1687 | } 1688 | .gantt_resizer_y .gantt_resizer_y { 1689 | position: absolute; 1690 | height: 20px; 1691 | width: 100%; 1692 | top: -10px; 1693 | left: 0; 1694 | z-index: 1; 1695 | } 1696 | .gantt_resizer_error { 1697 | background: #cd5c5c !important; 1698 | } 1699 | .gantt_layout_cell_border_left { 1700 | border-left: 1px solid #a4bed4; 1701 | } 1702 | .gantt_layout_cell_border_right { 1703 | border-right: 1px solid #a4bed4; 1704 | } 1705 | .gantt_layout_cell_border_top { 1706 | border-top: 1px solid #a4bed4; 1707 | } 1708 | .gantt_layout_cell_border_bottom { 1709 | border-bottom: 1px solid #a4bed4; 1710 | } 1711 | .gantt_layout_cell_border_transparent { 1712 | border-color: transparent; 1713 | } 1714 | .gantt_window { 1715 | position: absolute; 1716 | top: 50%; 1717 | left: 50%; 1718 | z-index: 999999999; 1719 | background: #fff; 1720 | } 1721 | .gantt_window_content { 1722 | position: relative; 1723 | } 1724 | .gantt_window_content_header { 1725 | background: #39c; 1726 | color: #fff; 1727 | height: 33px; 1728 | padding: 10px 10px 0; 1729 | border-bottom: 2px solid #fff; 1730 | position: relative; 1731 | } 1732 | .gantt_window_content_header_text { 1733 | padding-left: 10%; 1734 | } 1735 | .gantt_window_content_header_buttons { 1736 | position: absolute; 1737 | top: 10px; 1738 | right: 10px; 1739 | } 1740 | .gantt_window_content_header_buttons:hover { 1741 | color: #000; 1742 | cursor: pointer; 1743 | } 1744 | .gantt_window_content_resizer { 1745 | position: absolute; 1746 | width: 15px; 1747 | height: 15px; 1748 | bottom: 0; 1749 | line-height: 15px; 1750 | right: -1px; 1751 | text-align: center; 1752 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAABZJREFUeAFjIAUwUshlpJDLSIhLGAAACQ4AFk79JaMAAAAASUVORK5CYII=); 1753 | cursor: nw-resize; 1754 | z-index: 999; 1755 | } 1756 | .gantt_window_content_frame { 1757 | position: absolute; 1758 | top: 0; 1759 | left: 0; 1760 | width: 100%; 1761 | height: 100%; 1762 | background: rgba(0, 0, 0, 0.1); 1763 | z-index: 9999; 1764 | } 1765 | .gantt_window_drag { 1766 | cursor: pointer !important; 1767 | } 1768 | .gantt_window_resizing { 1769 | overflow: visible; 1770 | } 1771 | .gantt_window_resizing_body { 1772 | overflow: hidden !important; 1773 | } 1774 | .gantt_window_modal { 1775 | background: rgba(0, 0, 0, 0.1); 1776 | z-index: 9999; 1777 | top: 0; 1778 | left: 0; 1779 | width: 100%; 1780 | height: 100%; 1781 | position: fixed; 1782 | } 1783 | .gantt_cal_light, 1784 | .gantt_cal_quick_info, 1785 | .gantt_container, 1786 | .gantt_message_area, 1787 | .gantt_modal_box, 1788 | .gantt_tooltip { 1789 | text-rendering: optimizeLegibility; 1790 | -webkit-font-smoothing: antialiased; 1791 | -moz-osx-font-smoothing: grayscale; 1792 | } 1793 | .gantt_noselect { 1794 | -moz-user-select: -moz-none; 1795 | -webkit-user-select: none; 1796 | -ms-user-select: none; 1797 | user-select: none; 1798 | } 1799 | .gantt_drag_marker { 1800 | position: absolute; 1801 | top: -1000px; 1802 | left: -1000px; 1803 | font-family: Tahoma; 1804 | font-size: 11px; 1805 | z-index: 1; 1806 | white-space: nowrap; 1807 | } 1808 | .gantt_drag_marker .gantt_tree_icon.gantt_blank, 1809 | .gantt_drag_marker .gantt_tree_icon.gantt_close, 1810 | .gantt_drag_marker .gantt_tree_icon.gantt_open, 1811 | .gantt_drag_marker .gantt_tree_indent { 1812 | display: none; 1813 | } 1814 | .gantt_drag_marker, 1815 | .gantt_drag_marker .gantt_row.odd { 1816 | background-color: #fff; 1817 | } 1818 | .gantt_drag_marker .gantt_row { 1819 | border-left: 1px solid #d2d2d2; 1820 | border-top: 1px solid #d2d2d2; 1821 | } 1822 | .gantt_drag_marker .gantt_cell { 1823 | border-color: #d2d2d2; 1824 | } 1825 | .gantt_row.gantt_over, 1826 | .gantt_task_row.gantt_over { 1827 | background-color: #0070fe; 1828 | } 1829 | .gantt_row.gantt_transparent .gantt_cell { 1830 | opacity: 0.7; 1831 | } 1832 | .gantt_task_row.gantt_transparent { 1833 | background-color: #e4f0ff; 1834 | } 1835 | .gantt_container_resize_watcher { 1836 | background: transparent; 1837 | width: 100%; 1838 | height: 100%; 1839 | position: absolute; 1840 | top: 0; 1841 | left: 0; 1842 | z-index: -1; 1843 | pointer-events: none; 1844 | border: 0; 1845 | box-sizing: border-box; 1846 | opacity: 0; 1847 | } 1848 | -------------------------------------------------------------------------------- /src/components/Gantt/Gantt.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { gantt } from 'dhtmlx-gantt'; 3 | import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'; 4 | import { attachEvent } from './GanttAttachEvent.js'; 5 | import { setGanttTemplates } from './GanttTemplates.js'; 6 | import { setGanttConfig } from './GanttConfig.js'; 7 | import { isValidVariable } from '../../functions/Common/CommonHelper.js'; 8 | 9 | const Gantt = (props) => { 10 | const containerRef = useRef(null); 11 | useEffect(() => { 12 | setGanttConfig(gantt); 13 | setGanttTemplates(gantt); 14 | attachEvent(gantt, props); 15 | gantt.init(containerRef.current); 16 | gantt.ext.zoom.setLevel(props.zoom); 17 | }, []); 18 | 19 | useEffect(() => { 20 | gantt.ext.zoom.setLevel(props.zoom); 21 | }, [props.zoom]); 22 | 23 | useEffect(() => { 24 | try { 25 | gantt.clearAll(); 26 | if (isValidVariable(props.issue) && props.issue.length != 0) { 27 | props.issue.map((issue) => { 28 | gantt.addTask(issue); 29 | if ('links' in issue) { 30 | issue.links.map((link) => { 31 | gantt.addLink(link); 32 | return null; 33 | }); 34 | } 35 | }); 36 | props.issue.map((issue) => { 37 | if (issue._parent !== "#0") { 38 | gantt.setParent(gantt.getTask(issue.id), issue._parent); 39 | } 40 | }); 41 | gantt.sort('due_date', false); 42 | } 43 | } catch (err) { 44 | gantt.message({ text: err, type: 'error' }); 45 | } 46 | }, [ 47 | props.issue 48 | ]); 49 | 50 | return ( 51 |
52 | ); 53 | }; 54 | 55 | export default Gantt; 56 | -------------------------------------------------------------------------------- /src/components/Gantt/GanttAttachEvent.js: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { 4 | calculateStartDate, 5 | calculateDueDate, 6 | } from '../../functions/Common/CommonHelper'; 7 | 8 | export const attachEvent = (gantt, props) => { 9 | gantt.attachEvent('onTaskDblClick', (gantt_task_id, e) => { 10 | props.openIssueAtBrowser(gantt_task_id); 11 | }); 12 | 13 | gantt.attachEvent('onTaskCreated', (gantt_task_id, e) => { 14 | props.openNewIssueAtBrowser(gantt_task_id); 15 | }); 16 | 17 | gantt.attachEvent('onAfterTaskUpdate', (id, gantt_task) => { 18 | props.updateIssueByAPI(gantt_task, gantt); 19 | }); 20 | 21 | gantt.attachEvent('onBeforeTaskUpdate', (id, mode, gantt_task) => { 22 | }); 23 | 24 | gantt.attachEvent('onAfterTaskMove', (id, parent) => { 25 | let gantt_task = gantt.getTask(id); 26 | // gantt_task._parent = parent; 27 | props.updateIssueByAPI(gantt_task, gantt); 28 | }); 29 | 30 | gantt.attachEvent("onAfterLinkAdd", function (id, item) { 31 | let afterlinkId = []; 32 | let afterlink = []; 33 | let addobj = item.target; 34 | let taskObj = gantt.getTask(addobj); 35 | let target = taskObj.$target; 36 | target.forEach(function (linkId) { 37 | let link = gantt.getLink(linkId); 38 | let linkid = link.target; 39 | let linkIds = link.source; 40 | afterlink.push({ type: '0', target: linkid, source: linkIds }); 41 | let relinkIds = linkIds.slice(1); 42 | if (relinkIds != '') { 43 | afterlinkId.push(relinkIds); 44 | } 45 | }); 46 | // let linkids = afterlinkId.join(','); 47 | // gantt.getTask(addobj).dependon = linkids; //changes task's data 48 | // gantt.updateTask(addobj); //renders the updated task 49 | gantt.getTask(addobj).dependon = afterlinkId; 50 | gantt.getTask(addobj).links = afterlink; 51 | gantt.updateTask(addobj); 52 | }); 53 | 54 | gantt.attachEvent("onAfterLinkDelete", function (id, item) { 55 | let afterlinkId = []; 56 | let afterlink = []; 57 | let addobj = item.target; 58 | let taskObj = gantt.getTask(addobj); 59 | let target = taskObj.$target; 60 | target.forEach(function (linkId) { 61 | let link = gantt.getLink(linkId); 62 | let linkid = link.target; 63 | let linkIds = link.source; 64 | afterlink.push({ type: '0', target: linkid, source: linkIds }); 65 | let relinkIds = linkIds.slice(1); 66 | if (relinkIds != '') { 67 | afterlinkId.push(relinkIds); 68 | } 69 | }); 70 | // let linkids = afterlinkId.join(','); 71 | // gantt.getTask(addobj).dependon = linkids; //changes task's data 72 | // gantt.updateTask(addobj); //renders the updated task 73 | gantt.getTask(addobj).dependon = afterlinkId; 74 | gantt.getTask(addobj).links = afterlink; 75 | gantt.updateTask(addobj); 76 | }); 77 | 78 | // Custom QuickInfo 79 | // https://docs.dhtmlx.com/gantt/desktop__quick_info.html 80 | gantt.attachEvent('onQuickInfo', (id) => { 81 | let gantt_task = gantt.getTask(id); 82 | gantt.locale.labels.detail_button = 'DETAIL'; 83 | gantt.$click.buttons.detail_button = (gantt_task_id) => { 84 | props.openIssueAtBrowser(gantt_task_id); 85 | return true; 86 | }; 87 | 88 | gantt.ext.quickInfo.setContent({ 89 | header: { 90 | title: '

Description

', 91 | // date: ReactDOMServeSr.renderToStaticMarkup().toString(), 92 | }, 93 | content: ReactDOMServer.renderToStaticMarkup( 94 |
95 |

{gantt_task.text}

96 | 97 |
98 | ).toString(), 99 | buttons: ['detail_button'], 100 | }); 101 | }); 102 | 103 | // Changing the displayed range dynamically 104 | // https://docs.dhtmlx.com/gantt/desktop__configuring_time_scale.html#range 105 | gantt.attachEvent('onTaskDrag', function (id, mode, task, original) { 106 | var state = gantt.getState(); 107 | var minDate = state.min_date, 108 | maxDate = state.max_date; 109 | 110 | var scaleStep = 111 | gantt.date.add(new Date(), state.scale_step, state.scale_unit) - 112 | new Date(); 113 | 114 | var showDate, 115 | repaint = false; 116 | if (mode == 'resize' || mode == 'move') { 117 | if (Math.abs(task.start_date - minDate) < scaleStep) { 118 | showDate = task.start_date; 119 | repaint = true; 120 | } else if (Math.abs(task.end_date - maxDate) < scaleStep) { 121 | showDate = task.end_date; 122 | repaint = true; 123 | } 124 | 125 | if (repaint) { 126 | gantt.render(); 127 | gantt.showDate(showDate); 128 | } 129 | } 130 | }); 131 | }; 132 | -------------------------------------------------------------------------------- /src/components/Gantt/GanttConfig.js: -------------------------------------------------------------------------------- 1 | const shortenDate = (date) => { 2 | if (Object.prototype.toString.call(date) !== '[object Date]') { 3 | return null; 4 | } 5 | const m = ('00' + (date.getMonth() + 1)).slice(-2); 6 | const d = ('00' + date.getDate()).slice(-2); 7 | const shorten_date = m + '/' + d; 8 | return shorten_date; 9 | }; 10 | 11 | export const setGanttConfig = (gantt) => { 12 | gantt.config.xml_date = '%Y/%m/%d %H:%i'; 13 | gantt.config.order_branch = true; 14 | gantt.config.order_branch_free = true; 15 | 16 | gantt.config.keep_grid_width = true; 17 | gantt.config.grid_resize = true; 18 | gantt.config.open_tree_initially = true; 19 | gantt.config.fit_tasks = true; 20 | gantt.config.show_grid = false; 21 | gantt.config.sort = true; 22 | 23 | gantt.config.columns = [ 24 | { 25 | name: 'id', 26 | label: 'No.', 27 | align: 'left', 28 | tree: true, 29 | width: '120', 30 | template: (obj) => { 31 | var befweek = new Date(); 32 | befweek.setDate(befweek.getDate() - 7); 33 | if (obj.update < befweek.toLocaleDateString()) { 34 | return ( 35 | obj.id + 36 | "i" 37 | ); 38 | } 39 | return obj.id; 40 | }, 41 | }, 42 | { 43 | name: 'start_date', 44 | label: 'Start ', 45 | align: 'center', 46 | width: '60', 47 | template: (obj) => { 48 | return shortenDate(obj.start_date); 49 | }, 50 | }, 51 | { 52 | name: 'due_date', 53 | label: 'Due ', 54 | align: 'center', 55 | width: '60', 56 | template: (obj) => { 57 | return shortenDate(obj.due_date); 58 | }, 59 | }, 60 | { name: 'assignee', label: 'Assignee', align: 'center', width: '130' }, 61 | { name: 'add', label: '', width: '30' }, 62 | ]; 63 | 64 | gantt.plugins({ 65 | quick_info: true, 66 | drag_timeline: true, 67 | }); 68 | gantt.showDate(new Date()); 69 | gantt.ext.zoom.init({ 70 | levels: [ 71 | { 72 | name: 'Days', 73 | scale_height: 30, 74 | min_column_width: 30, 75 | scales: [ 76 | { unit: 'month', step: 1, format: '%n' }, 77 | { unit: 'day', step: 1, format: '%d' }, 78 | ], 79 | }, 80 | { 81 | name: 'Weeks', 82 | scale_height: 30, 83 | min_column_width: 20, 84 | scales: [{ unit: 'week', step: 1, format: '%n/%d~' }], 85 | }, 86 | { 87 | name: 'Years', 88 | scale_height: 30, 89 | column_width: 50, 90 | scales: [ 91 | { unit: 'year', step: 1, format: '%Y' }, 92 | { unit: 'month', step: 1, format: '%n' } 93 | ], 94 | }, 95 | ], 96 | useKey: "ctrlKey", 97 | trigger: "wheel", 98 | element: function(){ 99 | return gantt.$root.querySelector(".gantt_task"); 100 | } 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /src/components/Gantt/GanttTemplates.js: -------------------------------------------------------------------------------- 1 | import { 2 | calculateDuration, 3 | calculateDueDate, 4 | } from '../../functions/Common/CommonHelper.js'; 5 | 6 | export const setGanttTemplates = (gantt) => { 7 | gantt.templates.timeline_cell_class = function (item, date) { 8 | if (Object.prototype.toString.call(date) !== '[object Date]') { 9 | return null; 10 | } 11 | var today = new Date(); 12 | if (date.getDate() === today.getDate() && date.getMonth() === today.getMonth()) { 13 | return 'today'; 14 | } 15 | if (date.getDay() === 0 || date.getDay() === 6) { 16 | return 'weekend'; 17 | } 18 | var yesterday = new Date(); 19 | yesterday.setDate(yesterday.getDate() - 1); 20 | if (date < yesterday) { 21 | return 'past_days'; 22 | } 23 | }; 24 | 25 | gantt.templates.task_text = function (start, end, task) { 26 | return task.text; 27 | }; 28 | 29 | gantt.templates.task_class = function (start, end, task) { 30 | if (task.progress == 1) { 31 | return ''; 32 | } 33 | if (task.progress < 0.01) { 34 | if (start <= new Date()) { 35 | return 'behind'; 36 | } 37 | } else if ( 38 | new Date( 39 | calculateDueDate( 40 | start, 41 | (calculateDuration(start, end) + 1) * task.progress 42 | ) 43 | ) < new Date() 44 | ) { 45 | return 'behind'; 46 | } 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/Gantt/index.js: -------------------------------------------------------------------------------- 1 | import Gantt from './Gantt'; 2 | import './Gantt.css'; 3 | export default Gantt; -------------------------------------------------------------------------------- /src/components/PageHeader/PageHeader.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamact/react-issue-ganttchart/70a2c26fffa1011fe02a72168f54ece90cdf2e80/src/components/PageHeader/PageHeader.css -------------------------------------------------------------------------------- /src/components/PageHeader/PageHeader.js: -------------------------------------------------------------------------------- 1 | import { Helmet } from "react-helmet"; 2 | 3 | const PageHeader = (props) => { 4 | return ( 5 |
6 | 7 | 8 | {props.title} 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default PageHeader; 15 | -------------------------------------------------------------------------------- /src/components/PageHeader/index.js: -------------------------------------------------------------------------------- 1 | import PageHeader from './PageHeader'; 2 | import './PageHeader.css'; 3 | export default PageHeader; -------------------------------------------------------------------------------- /src/components/Toolbar/Toolbar.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lamact/react-issue-ganttchart/70a2c26fffa1011fe02a72168f54ece90cdf2e80/src/components/Toolbar/Toolbar.css -------------------------------------------------------------------------------- /src/components/Toolbar/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import TextField from '@material-ui/core/TextField'; 5 | import Button from '@material-ui/core/Button'; 6 | import ButtonGroup from '@material-ui/core/ButtonGroup'; 7 | import { Multiselect } from 'multiselect-react-dropdown'; 8 | import Autocomplete from '@material-ui/lab/Autocomplete'; 9 | import GitHubIcon from '@material-ui/icons/GitHub'; 10 | import { gantt } from 'dhtmlx-gantt'; 11 | import MenuOpenIcon from '@material-ui/icons/MenuOpen'; 12 | import { bake_cookie } from 'sfcookies'; 13 | 14 | const Toolbar = (props) => { 15 | const { classes } = props; 16 | return ( 17 |
18 | 19 | { 21 | gantt.config.show_grid = !gantt.config.show_grid; 22 | bake_cookie('menu_opened', gantt.config.show_grid); 23 | gantt.render(); 24 | }} /> 25 | 26 | { 33 | props.onGitURLChange(e.target.value); 34 | }} 35 | inputRef={props.register} 36 | name="git_url" 37 | /> 38 | { 45 | props.onTokenChange(e.target.value); 46 | }} 47 | inputRef={props.register} 48 | name="token" 49 | /> 50 | { 55 | props.onSelectedLabelChange(options); 56 | }} 57 | onRemove={(options) => { 58 | props.onSelectedLabelChange(options); 59 | }} 60 | displayValue="name" 61 | style={selector_style} 62 | placeholder="filter by labels" 63 | hidePlaceholder="false" 64 | emptyRecordMsg="No Labels" 65 | closeIcon="cancel" 66 | /> 67 | option.name} 72 | value={props.selected_assignee} 73 | noOptionsText="Requires a valid token" 74 | onChange={(e, assignee) => { 75 | props.onSelectedAssigneeChange(assignee); 76 | }} 77 | style={{ 78 | width: '15%', 79 | verticalAlign: 'middle', 80 | display: 'inline-block', 81 | marginRight: '15px', 82 | }} 83 | renderInput={(params) => ( 84 | 90 | )} 91 | /> 92 | 93 | 100 | 107 | 114 | 115 | 116 | window.open('https://github.com/lamact/react-issue-ganttchart')} /> 117 | 118 | 119 | ); 120 | }; 121 | 122 | const styles = (theme) => ({ 123 | root: { 124 | '& > *': { 125 | fontSize: '13px', 126 | marginRight: '4px', 127 | }, 128 | }, 129 | }); 130 | 131 | const selector_style = { 132 | multiselectContainer: { 133 | width: '27%', 134 | display: 'inline-block', 135 | verticalAlign: 'middle', 136 | padding: '4px', 137 | alignItems: 'flex-end', 138 | }, 139 | chips: { 140 | background: 'light blue', 141 | fontSize: '15px', 142 | }, 143 | searchBox: { 144 | border: 'none', 145 | }, 146 | }; 147 | 148 | export default withStyles(styles)(Toolbar); 149 | -------------------------------------------------------------------------------- /src/components/Toolbar/index.js: -------------------------------------------------------------------------------- 1 | import Toolbar from './Toolbar'; 2 | import './Toolbar.css'; 3 | export default Toolbar; -------------------------------------------------------------------------------- /src/functions/Common/CommonHelper.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export const isValidVariable = (variable) => { 4 | if ( 5 | variable !== null && 6 | variable !== [] && 7 | variable !== void 0 && 8 | variable !== '' 9 | ) { 10 | return true; 11 | } 12 | if (Array.isArray(variable)) { 13 | return variable.length > 0; 14 | } 15 | return false; 16 | }; 17 | 18 | export const validVariable = (variable) => { 19 | if (isValidVariable(variable)) { 20 | return variable; 21 | } else { 22 | return ''; 23 | } 24 | }; 25 | 26 | export const isValidIDName = (id_name) => { 27 | return isValidVariable(id_name) && 'id' in id_name && 'name' in id_name; 28 | }; 29 | 30 | export const isValidURL = (url) => { 31 | if (!isValidVariable(url)) { 32 | return false; 33 | } 34 | return /https?:\/\//.test(url); 35 | }; 36 | 37 | export const isNumber = (n) => { 38 | if (typeof n === 'number' && Number.isFinite(n)) { 39 | return true; 40 | } 41 | return false; 42 | }; 43 | 44 | export const orgRound = (value, base) => { 45 | return Math.round(value / base) * base; 46 | }; 47 | 48 | export const calculateDuration = (start_date, due_date) => { 49 | const start_date_moment = moment(start_date, 'YYYY/MM/DD'); 50 | const due_date_moment = moment(due_date, 'YYYY/MM/DD'); 51 | return due_date_moment.diff(start_date_moment, 'days') + 1; 52 | }; 53 | 54 | export const calculateStartDate = (due_date_str, duration) => { 55 | const due_date = new Date(due_date_str); 56 | const start_date = moment(due_date, 'YYYY/MM/DD') 57 | .add(-duration, 'd') 58 | .toDate(); 59 | return date2string(start_date); 60 | }; 61 | 62 | export const calculateDueDate = (start_date_str, duration) => { 63 | const start_date = new Date(start_date_str); 64 | const due_date = moment(start_date, 'YYYY/MM/DD') 65 | .add(duration - 1, 'd') 66 | .toDate(); 67 | return date2string(due_date); 68 | }; 69 | 70 | export const date2string = (date) => { 71 | if (Object.prototype.toString.call(date) !== '[object Date]') { 72 | return null; 73 | } else if (isNaN(date.getFullYear())) { 74 | return null; 75 | } 76 | 77 | let string = date.toLocaleDateString('ja-JP'); 78 | if (!/\d{4}\/\d{1,2}\/\d{1,2}/.test(string)) { 79 | const year = date.getFullYear(); 80 | const month = date.getMonth() + 1; 81 | const day = date.getDate(); 82 | string = year + '/' + month + '/' + day; 83 | } 84 | return string; 85 | }; 86 | 87 | export const adjustDateString = (date_str) => { 88 | return date2string(new Date(date_str)); 89 | }; 90 | 91 | export const getGanttStartDate = (start_date, due_date, created_at) => { 92 | let start_date_str = null; 93 | if (isValidVariable(start_date)) { 94 | start_date_str = date2string(start_date); 95 | } else if (isValidVariable(created_at)) { 96 | start_date_str = adjustDateString(created_at); 97 | } 98 | return start_date_str; 99 | }; 100 | 101 | export const getGanttDueDate = (start_date, due_date, created_at) => { 102 | let _due_date = null; 103 | if (isValidVariable(due_date)) { 104 | _due_date = new Date(due_date); 105 | } else if (isValidVariable(created_at)) { 106 | _due_date = new Date(created_at); 107 | } 108 | return _due_date; 109 | }; 110 | 111 | export const getGanttUpdateDate = (created_at, updated_at) => { 112 | let updated_date_str = null; 113 | if (updated_at != null) { 114 | updated_date_str = adjustDateString(updated_at); 115 | } else { 116 | updated_date_str = adjustDateString(created_at); 117 | } 118 | return updated_date_str; 119 | }; 120 | 121 | export const getGanttDuration = (start_date, due_date, created_at) => { 122 | let duration = null; 123 | if (!isValidVariable(due_date)) { 124 | return null; 125 | } 126 | if (!isValidVariable(start_date)) { 127 | start_date = created_at; 128 | } 129 | if (start_date != null && due_date != null) { 130 | duration = calculateDuration(start_date, due_date); 131 | } else { 132 | duration = 1; 133 | } 134 | return duration; 135 | }; 136 | 137 | export const ArrangeGanttTaskToGeneratedGanttTaskForCompare = (issue_info) => { 138 | let arrangelink = []; 139 | issue_info.links.map((list) => { 140 | arrangelink.push({ type: list.type, target: list.target, source: list.source }); 141 | }); 142 | var _parent 143 | if (issue_info.parent == 0) 144 | _parent = "#0"; 145 | else { 146 | _parent = issue_info.parent; 147 | } 148 | 149 | const arrange = { 150 | id: issue_info.id, 151 | text: issue_info.text, 152 | start_date: adjustDateString(issue_info.start_date), 153 | due_date: issue_info.due_date, 154 | duration: issue_info.duration, 155 | progress: issue_info.progress, 156 | assignee: issue_info.assignee, 157 | description: issue_info.description, 158 | update: issue_info.update, 159 | links: arrangelink, 160 | _parent: _parent, 161 | } 162 | return arrange; 163 | }; 164 | 165 | export const isEqualGanntTask = (GanntTaskA, GanntTaskB) => { 166 | return ( 167 | GanntTaskA.id == GanntTaskB.id && 168 | GanntTaskA.text == GanntTaskB.text && 169 | GanntTaskA.start_date == GanntTaskB.start_date && 170 | GanntTaskA.due_date == GanntTaskB.due_date.toString() && 171 | GanntTaskA.duration == GanntTaskB.duration && 172 | GanntTaskA.progress == GanntTaskB.progress && 173 | GanntTaskA.assignee == GanntTaskB.assignee && 174 | // GanntTaskA.description == GanntTaskB.description && 175 | GanntTaskA.update == GanntTaskB.update && 176 | GanntTaskA._parent == GanntTaskB._parent && 177 | JSON.stringify(GanntTaskA.links) == JSON.stringify(GanntTaskB.links) 178 | ); 179 | }; 180 | -------------------------------------------------------------------------------- /src/functions/Common/CommonHelper.test.js: -------------------------------------------------------------------------------- 1 | import { isValidIDName, isValidVariable, validVariable, getGanttStartDate, getGanttDueDate, getGanttDuration } from './CommonHelper'; 2 | 3 | describe('isValidVariable', () => { 4 | test('true', () => { 5 | expect(isValidVariable('aa')).toBe(true); 6 | }); 7 | test('true', () => { 8 | expect(isValidVariable(224)).toBe(true); 9 | }); 10 | test('true', () => { 11 | expect(isValidVariable(['a', 'b'])).toBe(true); 12 | }); 13 | test('true', () => { 14 | expect(isValidVariable(new Date())).toBe(true); 15 | }); 16 | test('false', () => { 17 | expect(isValidVariable(null)).toBe(false); 18 | }); 19 | test('false', () => { 20 | expect(isValidVariable(undefined)).toBe(false); 21 | }); 22 | test('false', () => { 23 | expect(isValidVariable('')).toBe(false); 24 | }); 25 | }); 26 | 27 | describe('validVariable', () => { 28 | test('true', () => { 29 | expect(validVariable('aa')).toBe('aa'); 30 | }); 31 | test('true', () => { 32 | expect(validVariable(null)).toBe(''); 33 | }); 34 | }); 35 | 36 | describe('isValidIDName', () => { 37 | test('true', () => { 38 | expect(isValidIDName({ id: 131, name: 'test1' })).toBe(true); 39 | }); 40 | test('false', () => { 41 | expect(isValidIDName(null)).toBe(false); 42 | }); 43 | test('false', () => { 44 | expect(isValidIDName({ id: 21 })).toBe(false); 45 | }); 46 | test('false', () => { 47 | expect(isValidIDName({ name: 'test1' })).toBe(false); 48 | }); 49 | }); 50 | 51 | describe('getGanttStartDate', () => { 52 | test('true', () => { 53 | expect(getGanttStartDate(new Date('2021/2/13'), new Date('2021/2/15'), new Date('2021/2/13'))).toBe('2021/2/13'); 54 | }); 55 | test('true', () => { 56 | expect(getGanttStartDate(null, new Date('2021/2/15'), new Date('2021/2/13'))).toBe('2021/2/13'); 57 | }); 58 | test('true', () => { 59 | expect(getGanttStartDate(new Date('2021/2/13'), null, new Date('2021/2/13'))).toBe('2021/2/13'); 60 | }); 61 | test('true', () => { 62 | expect(getGanttStartDate(null, null, new Date('2021/2/13'))).toBe('2021/2/13'); 63 | }); 64 | }); 65 | 66 | 67 | describe('getGanttDueDate', () => { 68 | test('true', () => { 69 | expect(getGanttDueDate(new Date('2021/2/13'), new Date('2021/2/15'), new Date('2021/2/13'))).toStrictEqual(new Date('2021/2/15')); 70 | }); 71 | test('true', () => { 72 | expect(getGanttDueDate(null, new Date('2021/2/15'), new Date('2021/2/13'))).toStrictEqual(new Date('2021/2/15')); 73 | }); 74 | test('true', () => { 75 | expect(getGanttDueDate(new Date('2021/2/13'), null, new Date('2021/2/13'))).toStrictEqual(new Date('2021/2/13')); 76 | }); 77 | test('true', () => { 78 | expect(getGanttDueDate(null, null, new Date('2021/2/13'))).toStrictEqual(new Date('2021/2/13')); 79 | }); 80 | }); 81 | 82 | 83 | describe('getGanttDuration', () => { 84 | test('true', () => { 85 | expect(getGanttDuration(new Date('2021/2/13'), new Date('2021/2/15'), new Date('2021/2/13'))).toStrictEqual(3); 86 | }); 87 | test('true', () => { 88 | expect(getGanttDuration(null, new Date('2021/2/15'), new Date('2021/2/13'))).toStrictEqual(3); 89 | }); 90 | test('true', () => { 91 | expect(getGanttDuration(null, null, new Date('2021/2/13'))).toStrictEqual(null); 92 | }); 93 | }); -------------------------------------------------------------------------------- /src/functions/Common/IssueAPI.js: -------------------------------------------------------------------------------- 1 | import { isGitHubURL } from '../GitHub/GitHubURLHelper.js'; 2 | import { 3 | isGitLabURL, 4 | getSelfHostingGitLabDomain, 5 | } from '../GitLab/GitLabURLHelper.js'; 6 | import { 7 | getGitHubIssuesFromAPI, 8 | updateGitHubIssueFromGanttTask, 9 | openGitHubIssueAtBrowser, 10 | openGitHubNewIssueAtBrowser, 11 | setGitHubLabelListOfRepoFromAPI, 12 | setGitHubMemberListOfRepoFromAPI, 13 | } from '../GitHub/GitHubAPI.js'; 14 | import { 15 | getGitLabIssuesFromAPI, 16 | updateGitLabIssueFromGanttTask, 17 | openGitLabIssueAtBrowser, 18 | openGitLabNewIssueAtBrowser, 19 | setGitLabLabelListOfRepoFromAPI, 20 | setGitLabMemberListOfRepoFromAPI, 21 | } from '../GitLab/GitLabAPI.js'; 22 | import { isValidURL } from '../Common/CommonHelper.js'; 23 | 24 | export const getIssuesFromAPI = async ( 25 | git_url, 26 | token, 27 | selected_labels, 28 | selected_assignee 29 | ) => { 30 | if (!isValidURL(git_url)) { 31 | return Promise.resolve(); 32 | } else if (isGitHubURL(git_url)) { 33 | return getGitHubIssuesFromAPI( 34 | git_url, 35 | token, 36 | selected_labels, 37 | selected_assignee 38 | ); 39 | } else if (isGitLabURL(git_url) || getSelfHostingGitLabDomain(git_url) !== null) { 40 | return getGitLabIssuesFromAPI( 41 | git_url, 42 | token, 43 | selected_labels, 44 | selected_assignee 45 | ); 46 | } 47 | }; 48 | 49 | export const setLabelListOfRepoFromAPI = async (git_url, token) => { 50 | if (!isValidURL(git_url)) { 51 | return Promise.resolve(); 52 | } else if (isGitHubURL(git_url)) { 53 | return setGitHubLabelListOfRepoFromAPI(git_url, token); 54 | } else if ( 55 | isGitLabURL(git_url) || 56 | getSelfHostingGitLabDomain(git_url) !== null 57 | ) { 58 | return setGitLabLabelListOfRepoFromAPI(git_url, token); 59 | } 60 | }; 61 | 62 | export const setMemberListOfRepoFromAPI = async (git_url, token) => { 63 | if (!isValidURL(git_url)) { 64 | return Promise.resolve(); 65 | } else if (isGitHubURL(git_url)) { 66 | return setGitHubMemberListOfRepoFromAPI(git_url, token); 67 | } else if ( 68 | isGitLabURL(git_url) || 69 | getSelfHostingGitLabDomain(git_url) !== null 70 | ) { 71 | return setGitLabMemberListOfRepoFromAPI(git_url, token); 72 | } 73 | }; 74 | 75 | export const updateIssueByAPI = (gantt_task, token, gantt, git_url) => { 76 | if (!isValidURL(git_url)) { 77 | return Promise.resolve(); 78 | } else if (isGitHubURL(git_url)) { 79 | return updateGitHubIssueFromGanttTask(gantt_task, token, gantt, git_url); 80 | } else if ( 81 | isGitLabURL(git_url) || 82 | getSelfHostingGitLabDomain(git_url) !== null 83 | ) { 84 | return updateGitLabIssueFromGanttTask(gantt_task, token, gantt, git_url); 85 | } 86 | }; 87 | 88 | export const openIssueAtBrowser = (gantt_task_id, git_url) => { 89 | if (!isValidURL(git_url)) { 90 | return Promise.resolve(); 91 | } else if (isGitHubURL(git_url)) { 92 | openGitHubIssueAtBrowser(gantt_task_id, git_url); 93 | } else if ( 94 | isGitLabURL(git_url) || 95 | getSelfHostingGitLabDomain(git_url) !== null 96 | ) { 97 | openGitLabIssueAtBrowser(gantt_task_id, git_url); 98 | } 99 | }; 100 | 101 | export const openNewIssueAtBrowser = (gantt_task, git_url) => { 102 | if (!isValidURL(git_url)) { 103 | return null; 104 | } else if (isGitHubURL(git_url)) { 105 | openGitHubNewIssueAtBrowser(gantt_task, git_url); 106 | } else if ( 107 | isGitLabURL(git_url) || 108 | getSelfHostingGitLabDomain(git_url) !== null 109 | ) { 110 | openGitLabNewIssueAtBrowser(gantt_task, git_url); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /src/functions/Common/IssueAPI.test.js: -------------------------------------------------------------------------------- 1 | describe('true is truthy', () => { 2 | test('true is truthy', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | 7 | // import MockAdapter from 'axios-mock-adapter'; 8 | // import axios from 'axios'; 9 | // import { getIssuesFromAPI } from './IssueAPI'; 10 | 11 | // describe('setGitHubLabelListOfRepoFromAPI', () => { 12 | // const mockAxios = new MockAdapter(axios); 13 | // mockAxios 14 | // .onGet( 15 | // 'https://gitlab.com/api/v4/projects/lamact%2Fsukima/issues?access_token=token&labels=&assignee_id=3666147&per_page=100&state=opened' 16 | // ) 17 | // .reply(200, [ 18 | // { 19 | // assignee: { 20 | // avatar_url: 21 | // 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/3666147/avatar.png', 22 | // id: 3666147, 23 | // name: 'yhzz', 24 | // state: 'active', 25 | // username: 'yhzz', 26 | // web_url: 'https://gitlab.com/yhzz', 27 | // }, 28 | // iid: 1, 29 | // name: 'aa', 30 | // created_at: new Date('2000/01/01'), 31 | // }, 32 | // ]); 33 | 34 | // test('true', () => { 35 | // return getIssuesFromAPI('https://gitlab.com/lamact/sukima', 'token', [], { 36 | // id: 3666147, 37 | // name: 'yhzz', 38 | // }).then((data) => { 39 | // expect(data).toStrictEqual([ 40 | // { 41 | // assignee: 'yhzz', 42 | // description: undefined, 43 | // due_date: new Date('2000/01/01'), 44 | // duration: 1, 45 | // id: '#1', 46 | // progress: null, 47 | // start_date: '2000/1/1', 48 | // text: undefined, 49 | // update: '2000/1/1', 50 | // links:[], 51 | // }, 52 | // ]); 53 | // }); 54 | // // expect(setGitHubLabelListOfRepoFromAPI(description, issue_info)).toEqual( 55 | // // gantt_task 56 | // // ); 57 | // // }); 58 | // }); 59 | // }); 60 | -------------------------------------------------------------------------------- /src/functions/Common/Parser.js: -------------------------------------------------------------------------------- 1 | import { isValidVariable, isValidIDName, isNumber } from './CommonHelper.js'; 2 | import yaml from 'js-yaml'; 3 | import { gantt } from 'dhtmlx-gantt'; 4 | 5 | export const removeFirstSharp = (id_str) => { 6 | if(!isValidVariable(id_str)){ 7 | return null; 8 | } 9 | if (id_str.length > 1 && /^#/.test(id_str)) { 10 | id_str = id_str.substring(1); 11 | } 12 | return id_str; 13 | }; 14 | 15 | export const removeLastSlash = (url) => { 16 | if(!isValidVariable(url)){ 17 | return null; 18 | } 19 | if (url.length > 1 && /\/$/.test(url)) { 20 | url = url.slice(0, -1); 21 | } 22 | return url; 23 | }; 24 | 25 | export const removeLastSpace = (url) => { 26 | if(!isValidVariable(url)){ 27 | return null; 28 | } 29 | if (url.length > 1 && / +$/.test(url)) { 30 | url = url.slice(0, -1); 31 | } 32 | return url; 33 | }; 34 | 35 | export const getYamlPartFromDescription = (description) => { 36 | if (description === null) { 37 | return null; 38 | } 39 | if (typeof description !== 'string') { 40 | return null; 41 | } 42 | let str = description.split(/^```yaml/); 43 | if (str === null || str.length < 2) { 44 | return null; 45 | } 46 | str = str[1].split(/```/); 47 | if (str === null || str.length < 2) { 48 | return null; 49 | } 50 | return str[0]; 51 | }; 52 | 53 | export const parseYamlFromDescription = (description) => { 54 | if (description === null) { 55 | return null; 56 | } 57 | const yaml_part = getYamlPartFromDescription(description); 58 | if (yaml_part === null) { 59 | return null; 60 | } 61 | 62 | let yaml_struct = null; 63 | try { 64 | yaml_struct = yaml.load(yaml_part); 65 | } catch (e) { 66 | gantt.message({ text: 'failed load yaml' + yaml_part, type: 'error' }); 67 | } 68 | return yaml_struct; 69 | }; 70 | 71 | export const getStringFromDescriptionYaml = (description, column_name) => { 72 | if (description === null) { 73 | return null; 74 | } 75 | const yaml_struct = parseYamlFromDescription(description); 76 | if (yaml_struct === null || !(column_name in yaml_struct)) { 77 | return null; 78 | } 79 | const string = yaml_struct[column_name]; 80 | if (typeof string !== 'string') { 81 | return null; 82 | } 83 | return removeLastSpace(removeLastSpace(string)); 84 | }; 85 | 86 | export const getNumberFromDescriptionYaml = (description, column_name) => { 87 | if (description === null) { 88 | return null; 89 | } 90 | const yaml_struct = parseYamlFromDescription(description); 91 | if (yaml_struct === null || !(column_name in yaml_struct)) { 92 | return null; 93 | } 94 | const number = yaml_struct[column_name]; 95 | if (typeof number !== 'number') { 96 | return null; 97 | } 98 | return number; 99 | }; 100 | 101 | export const getDateFromDescriptionYaml = (description, column_name) => { 102 | if (description === null) { 103 | return null; 104 | } 105 | const date = getStringFromDescriptionYaml(description, column_name); 106 | if (!/\d{4}\/\d{1,2}\/\d{1,2}/.test(date)) { 107 | return null; 108 | } 109 | return new Date(date); 110 | }; 111 | 112 | export const replacePropertyInDescriptionString = (description, task) => { 113 | if (description === null || task === null) { 114 | return null; 115 | } 116 | let task_section = yaml.dump(task); 117 | task_section = 118 | `\`\`\`yaml 119 | ` + 120 | task_section + 121 | `\`\`\``; 122 | let str = description.split(/^```yaml/); 123 | if (str === null || str.length < 2) { 124 | if (/```/.test(description)) { 125 | return null; 126 | } 127 | return task_section + '\n' + description; 128 | } 129 | const first_section = str[0]; 130 | str = str[1].split(/```/); 131 | if (str === null || str.length < 2) { 132 | return null; 133 | } 134 | const end_section = str[1]; 135 | if (first_section == null || end_section == null) { 136 | return null; 137 | } 138 | return first_section + task_section + end_section; 139 | }; 140 | 141 | export const convertIDNameListToString = (list) => { 142 | let string = ''; 143 | if (isValidVariable(list)) { 144 | list.map((info) => { 145 | if (isValidIDName(info) && isValidVariable(info.id)) { 146 | string += info.id + ':' + info.name + ','; 147 | } 148 | return null; 149 | }); 150 | return string; 151 | } 152 | return null; 153 | }; 154 | 155 | export const convertIDNamesStringToList = (string) => { 156 | let list = []; 157 | if (isValidVariable(string)) { 158 | const split_string = string.split(','); 159 | split_string.forEach((element, index, array) => { 160 | if (index < split_string.length - 1) { 161 | const info = element.split(':'); 162 | if (!isNaN(parseInt(info[0]))) { 163 | const label = { 164 | id: parseInt(info[0]), 165 | name: info[1], 166 | }; 167 | list.push(label); 168 | } 169 | } 170 | }); 171 | } else { 172 | list = [{ id: 0, name: '' }]; 173 | } 174 | return list; 175 | }; 176 | 177 | export const getDependonFromDescriptionYaml = (description, column_name) => { 178 | if (description === null) { 179 | return null; 180 | } 181 | const yaml_struct = parseYamlFromDescription(description); 182 | if (yaml_struct === null || !(column_name in yaml_struct)) { 183 | return null; 184 | } 185 | const number = yaml_struct[column_name]; 186 | return number; 187 | }; 188 | -------------------------------------------------------------------------------- /src/functions/Common/Parser.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | removeFirstSharp, 3 | removeLastSlash, 4 | removeLastSpace, 5 | getStringFromDescriptionYaml, 6 | getDateFromDescriptionYaml, 7 | getNumberFromDescriptionYaml, 8 | replacePropertyInDescriptionString, 9 | parseYamlFromDescription, 10 | getYamlPartFromDescription, 11 | convertIDNameListToString, 12 | convertIDNamesStringToList, 13 | } from './Parser'; 14 | 15 | describe('removeFirstSharp', () => { 16 | test('#0', () => { 17 | expect(removeFirstSharp('#0')).toBe('0'); 18 | }); 19 | 20 | test('#1', () => { 21 | expect(removeFirstSharp('#1')).toBe('1'); 22 | }); 23 | 24 | test('#1234', () => { 25 | expect(removeFirstSharp('#1234')).toBe('1234'); 26 | }); 27 | 28 | test('null', () => { 29 | expect(removeFirstSharp(null)).toBe(null); 30 | }); 31 | }); 32 | 33 | describe('removeLastSlash', () => { 34 | test('a/', () => { 35 | expect(removeLastSlash('a/')).toBe('a'); 36 | }); 37 | 38 | test('a/a', () => { 39 | expect(removeLastSlash('a/a')).toBe('a/a'); 40 | }); 41 | 42 | test('null', () => { 43 | expect(removeLastSlash(null)).toBe(null); 44 | }); 45 | }); 46 | 47 | describe('removeLastSpace', () => { 48 | test('a ', () => { 49 | expect(removeLastSpace('a ')).toBe('a'); 50 | }); 51 | 52 | test('a a', () => { 53 | expect(removeLastSpace('a a')).toBe('a a'); 54 | }); 55 | 56 | test('null', () => { 57 | expect(removeLastSpace(null)).toBe(null); 58 | }); 59 | }); 60 | 61 | const failed_yaml_description_part = `\`\`\`yaml 62 | progress: 0 63 | parent: 2 64 | `; 65 | 66 | const failed_yaml_description_part3 = `\`\`\`yaml 67 | progress: 0 a: 68 | parent: 2 69 | \`\`\` 70 | `; 71 | 72 | const success_yaml_description_struct = { 73 | due_date: '2021/2/13', 74 | parent: 2, 75 | progress: 0, 76 | start_date: '2021/2/12', 77 | }; 78 | 79 | describe('parseYamlFromDescription', () => { 80 | test('start_date', () => { 81 | expect(parseYamlFromDescription(success_yaml_description)).toEqual( 82 | success_yaml_description_struct 83 | ); 84 | }); 85 | test('null', () => { 86 | expect(parseYamlFromDescription(null)).toBe(null); 87 | }); 88 | test('null', () => { 89 | expect(parseYamlFromDescription(failed_yaml_description_part)).toBe(null); 90 | }); 91 | test('null', () => { 92 | expect(parseYamlFromDescription(failed_yaml_description_part3)).toBe(null); 93 | }); 94 | }); 95 | 96 | const success_yaml_description = `\`\`\`yaml 97 | progress: 0 98 | start_date: 2021/2/12 99 | due_date: 2021/2/13 100 | parent: 2 101 | \`\`\` 102 | 103 | ## 概要 104 | issueの内容 105 | `; 106 | 107 | const success_yaml_description_part = ` 108 | progress: 0 109 | start_date: 2021/2/12 110 | due_date: 2021/2/13 111 | parent: 2 112 | `; 113 | 114 | const failed_yaml_description = `\`\`\`yaml 115 | progress: 0 116 | parent: 2 117 | \`\`\` 118 | start_date: 2021/2/12 119 | due_date: 2021/2/13 120 | 121 | ## 概要 122 | issueの内容 123 | `; 124 | 125 | const failed_yaml_description_part2 = `\`\`\`yaml 126 | progress: 0 127 | parent: 2 128 | start_date: 2021/2/12 129 | due_date: 2021/2/13 130 | 131 | ## 概要 132 | issueの内容 133 | `; 134 | 135 | const failed_yaml_description_part4 = `\`\`\`yaml 136 | progress: 0 137 | parent: 2 138 | start_date: 2021/2/123 139 | due_date: 2021/2/13 140 | 141 | ## 概要 142 | issueの内容 143 | `; 144 | 145 | describe('getYamlPartFromDescription', () => { 146 | test('true', () => { 147 | expect(getYamlPartFromDescription(success_yaml_description)).toEqual( 148 | success_yaml_description_part 149 | ); 150 | }); 151 | test('null', () => { 152 | expect(getYamlPartFromDescription(null)).toBe(null); 153 | }); 154 | test('null', () => { 155 | expect(getYamlPartFromDescription(12)).toBe(null); 156 | }); 157 | test('null', () => { 158 | expect(getYamlPartFromDescription('12')).toBe(null); 159 | }); 160 | test('null', () => { 161 | expect(getYamlPartFromDescription(failed_yaml_description_part2)).toBe( 162 | null 163 | ); 164 | }); 165 | }); 166 | 167 | describe('getStringFromDescriptionYaml', () => { 168 | test('start_date', () => { 169 | expect( 170 | getStringFromDescriptionYaml(success_yaml_description, 'start_date') 171 | ).toBe('2021/2/12'); 172 | }); 173 | test('null', () => { 174 | expect(getStringFromDescriptionYaml(success_yaml_description, null)).toBe( 175 | null 176 | ); 177 | }); 178 | test('null', () => { 179 | expect(getStringFromDescriptionYaml(success_yaml_description, 23)).toBe( 180 | null 181 | ); 182 | }); 183 | test('null', () => { 184 | expect( 185 | getStringFromDescriptionYaml(success_yaml_description, 'progress') 186 | ).toBe(null); 187 | }); 188 | test('null', () => { 189 | expect(getStringFromDescriptionYaml(null, 'start_date')).toBe(null); 190 | }); 191 | test('null', () => { 192 | expect( 193 | getStringFromDescriptionYaml(failed_yaml_description, 'start_date') 194 | ).toBe(null); 195 | }); 196 | test('due_date', () => { 197 | expect( 198 | getStringFromDescriptionYaml(success_yaml_description, 'due_date') 199 | ).toBe('2021/2/13'); 200 | }); 201 | test('start_date', () => { 202 | expect( 203 | getDateFromDescriptionYaml(success_yaml_description, 'start_date') 204 | ).toEqual(new Date('2021/2/12')); 205 | }); 206 | test('due_date', () => { 207 | expect( 208 | getDateFromDescriptionYaml(success_yaml_description, 'due_date') 209 | ).toEqual(new Date('2021/2/13')); 210 | }); 211 | test('null', () => { 212 | expect(getDateFromDescriptionYaml(null, 'due_date')).toBe(null); 213 | }); 214 | test('null', () => { 215 | expect( 216 | getDateFromDescriptionYaml(failed_yaml_description_part4, 'start_date') 217 | ).toBe(null); 218 | }); 219 | test('progress', () => { 220 | expect( 221 | getNumberFromDescriptionYaml(success_yaml_description, 'progress') 222 | ).toBe(0); 223 | }); 224 | test('null', () => { 225 | expect(getNumberFromDescriptionYaml(null, 'progress')).toBe(null); 226 | }); 227 | test('parent', () => { 228 | expect( 229 | getNumberFromDescriptionYaml(success_yaml_description, 'parent') 230 | ).toBe(2); 231 | }); 232 | test('null', () => { 233 | expect( 234 | getNumberFromDescriptionYaml(success_yaml_description, 'start_date') 235 | ).toBe(null); 236 | }); 237 | }); 238 | 239 | const chenged_task = { 240 | start_date: '2021/2/21', 241 | due_date: '2021/2/13', 242 | progress: 0.1, 243 | parent: 5, 244 | }; 245 | 246 | const success_changed_yaml = `\`\`\`yaml 247 | start_date: 2021/2/21 248 | due_date: 2021/2/13 249 | progress: 0.1 250 | parent: 5 251 | \`\`\` 252 | 253 | ## 概要 254 | issueの内容 255 | `; 256 | 257 | const success_changed_yaml2 = ` 258 | ## 概要 259 | issueの内容 260 | `; 261 | 262 | const failed_changed_yaml = `\`\`\` 263 | start_date: 2021/2/21 264 | due_date: 2021/2/13 265 | progress: 0.1 266 | parent: 5 267 | \`\`\` 268 | 269 | ## 概要 270 | issueの内容 271 | `; 272 | 273 | const failed_changed_yaml2 = `\`\`\`yaml 274 | start_date: 2021/2/21 275 | due_date: 2021/2/13 276 | progress: 0.1 277 | parent: 5 278 | 279 | ## 概要 280 | issueの内容 281 | `; 282 | 283 | describe('replacePropertyInDescriptionString', () => { 284 | test('start_date', () => { 285 | expect( 286 | replacePropertyInDescriptionString(success_yaml_description, chenged_task) 287 | ).toBe(success_changed_yaml); 288 | }); 289 | test('start_date', () => { 290 | expect( 291 | replacePropertyInDescriptionString(success_changed_yaml, chenged_task) 292 | ).toBe(success_changed_yaml); 293 | }); 294 | test('success_changed_yaml2', () => { 295 | expect( 296 | replacePropertyInDescriptionString(success_changed_yaml2, chenged_task) 297 | ).toBe(success_changed_yaml); 298 | }); 299 | test('null', () => { 300 | expect(replacePropertyInDescriptionString(null, chenged_task)).toBe(null); 301 | }); 302 | test('null', () => { 303 | expect(replacePropertyInDescriptionString(success_changed_yaml, null)).toBe( 304 | null 305 | ); 306 | }); 307 | test('null', () => { 308 | expect( 309 | replacePropertyInDescriptionString(failed_changed_yaml, chenged_task) 310 | ).toBe(null); 311 | }); 312 | test('null', () => { 313 | expect( 314 | replacePropertyInDescriptionString(failed_changed_yaml2, chenged_task) 315 | ).toBe(null); 316 | }); 317 | }); 318 | 319 | const id_name_list = [ 320 | { id: 131, name: 'test1' }, 321 | { id: 124, name: 'test2' }, 322 | { id: 421, name: 'test3' }, 323 | ]; 324 | 325 | const id_name_string = '131:test1,124:test2,421:test3,'; 326 | 327 | const failed_id_name_string = '131:test1,string:test2,421:test3,'; 328 | 329 | const failed_id_name_list = [ 330 | { id: 131, name: 'test1' }, 331 | { id: 421, name: 'test3' }, 332 | ]; 333 | describe('convertIDNameListToString', () => { 334 | test('true', () => { 335 | expect(convertIDNameListToString(id_name_list)).toBe(id_name_string); 336 | }); 337 | test('null', () => { 338 | expect(convertIDNameListToString(null)).toBe(null); 339 | }); 340 | }); 341 | 342 | describe('convertIDNamesStringToList', () => { 343 | test('true', () => { 344 | expect(convertIDNamesStringToList(id_name_string)).toEqual(id_name_list); 345 | }); 346 | test('null', () => { 347 | expect(convertIDNamesStringToList(null)).toEqual([{ id: 0, name: '' }]); 348 | }); 349 | test('null', () => { 350 | expect(convertIDNamesStringToList(failed_id_name_string)).toEqual( 351 | failed_id_name_list 352 | ); 353 | }); 354 | }); 355 | -------------------------------------------------------------------------------- /src/functions/GitHub/GitHubAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { 3 | getGitHubAPIURLIssuebyNumber, 4 | getGitHubAPIURLIssueFilterd, 5 | getGitHubAPIURLLabel, 6 | getGitHubAPIURLCollaborators, 7 | getGitHubURLIssuebyNumber, 8 | getGitHubURLNewIssueWithTemplate, 9 | } from './GitHubURLHelper.js'; 10 | import { 11 | generateGanttTaskFromGitHub, 12 | updateGitHubDescriptionStringFromGanttTask, 13 | } from './GitHubHelper.js'; 14 | import { 15 | date2string, 16 | isValidVariable, 17 | isEqualGanntTask, 18 | ArrangeGanttTaskToGeneratedGanttTaskForCompare, 19 | } from '../Common/CommonHelper.js'; 20 | import { 21 | removeFirstSharp, 22 | replacePropertyInDescriptionString, 23 | } from '../Common/Parser.js'; 24 | 25 | export const getGitHubIssueFromAPI = async (git_url, token, issue_info) => { 26 | return axios 27 | .get(getGitHubAPIURLIssuebyNumber(git_url, issue_info.number), { 28 | headers: { Authorization: `Bearer ${token}` }, 29 | data: {}, 30 | }) 31 | .then((res) => { 32 | return generateGanttTaskFromGitHub(res.data.body, issue_info); 33 | }) 34 | .catch((err) => { 35 | return Promise.reject(err); 36 | }); 37 | }; 38 | 39 | export const getGitHubIssuesFromAPI = async ( 40 | git_url, 41 | token, 42 | selected_labels, 43 | selected_assignee 44 | ) => { 45 | return axios 46 | .get( 47 | getGitHubAPIURLIssueFilterd(git_url, selected_labels, selected_assignee), { 48 | headers: { Authorization: `Bearer ${token}` }, 49 | data: {}, 50 | }) 51 | .then((res) => { 52 | const promise_list = []; 53 | res.data.map((issue_info) => { 54 | promise_list.push(getGitHubIssueFromAPI(git_url, token, issue_info)); 55 | }); 56 | return Promise.all(promise_list); 57 | }) 58 | .catch((err) => { 59 | return Promise.reject(err); 60 | }); 61 | }; 62 | 63 | export const setGitHubLabelListOfRepoFromAPI = async (git_url, token) => { 64 | return axios.get(getGitHubAPIURLLabel(git_url), { 65 | headers: { Authorization: `Bearer ${token}` }, 66 | data: {}, 67 | }).then((res) => { 68 | let labels = []; 69 | res.data.map((info) => { 70 | labels.push({ id: info.id, name: info.name }); 71 | return null; 72 | }); 73 | return labels; 74 | }); 75 | }; 76 | 77 | export const setGitHubMemberListOfRepoFromAPI = async (git_url, token) => { 78 | if ( 79 | isValidVariable(token) && 80 | token !== 'Tokens that have not yet been entered' 81 | ) { 82 | return axios 83 | .get(getGitHubAPIURLCollaborators(git_url), { 84 | headers: { Authorization: `Bearer ${token}` }, 85 | data: {}, 86 | }) 87 | .then((res) => { 88 | let members = []; 89 | res.data.map((info) => { 90 | members.push({ id: info.id, name: info.login }); 91 | return null; 92 | }); 93 | return members; 94 | }); 95 | } else { 96 | console.warn('token is not valid!'); 97 | } 98 | }; 99 | 100 | export const updateGitHubIssueFromGanttTask = ( 101 | gantt_task, 102 | token, 103 | gantt, 104 | git_url 105 | ) => { 106 | const url = getGitHubAPIURLIssuebyNumber( 107 | git_url, 108 | removeFirstSharp(gantt_task.id) 109 | ); 110 | axios 111 | .get(url, { 112 | headers: { Authorization: `Bearer ${token}` }, 113 | data: {}, 114 | }) 115 | .then((res) => { 116 | const issue_info = res.data; 117 | if ( 118 | updateGitHubDescriptionStringFromGanttTask( 119 | issue_info.body, 120 | gantt_task 121 | ) == null 122 | ) { 123 | gantt.message({ 124 | text: 'failed update issue. ' + gantt_task.text, 125 | type: 'error', 126 | }); 127 | } else { 128 | // Update if different from existing parameters 129 | if (!isEqualGanntTask( 130 | ArrangeGanttTaskToGeneratedGanttTaskForCompare(gantt_task), 131 | generateGanttTaskFromGitHub(issue_info.body, issue_info) 132 | )) { 133 | axios 134 | .post( 135 | url, 136 | { 137 | body: updateGitHubDescriptionStringFromGanttTask( 138 | issue_info.body, 139 | gantt_task 140 | ), 141 | }, 142 | { 143 | headers: { 144 | Authorization: `Bearer ${token}`, 145 | }, 146 | } 147 | ) 148 | .then((res) => { 149 | gantt.message({ 150 | text: 'success update issue. ' + gantt_task.id, 151 | }); 152 | }) 153 | .catch((err) => { 154 | gantt.message({ 155 | text: 'failed update GitHub issue. check your token.' + err, 156 | type: 'error', 157 | }); 158 | }); 159 | } 160 | } 161 | }) 162 | .catch((err) => { 163 | gantt.message({ 164 | text: 'failed get GitHub issue. check your url.' + err, 165 | type: 'error', 166 | }); 167 | getGitHubIssuesFromAPI(gantt, token, git_url); 168 | }); 169 | return null; 170 | }; 171 | 172 | export const openGitHubIssueAtBrowser = (gantt_task_id, git_url) => { 173 | window.open( 174 | getGitHubURLIssuebyNumber(git_url, removeFirstSharp(gantt_task_id)), 175 | '_blank' 176 | ); 177 | }; 178 | 179 | export const openGitHubNewIssueAtBrowser = (gantt_task, git_url) => { 180 | const start_date_str = date2string(new Date()); 181 | const due_date_str = date2string(new Date()); 182 | if (gantt_task.parent == null) { 183 | gantt_task.parent = 0; 184 | } 185 | const task = { 186 | start_date: start_date_str, 187 | due_date: due_date_str, 188 | progress: 0.1, 189 | parent: parseInt(removeFirstSharp(gantt_task.parent)), 190 | }; 191 | let body = replacePropertyInDescriptionString('', task); 192 | body = encodeURIComponent(body); 193 | window.open(getGitHubURLNewIssueWithTemplate(git_url) + body, '_blank'); 194 | }; 195 | -------------------------------------------------------------------------------- /src/functions/GitHub/GitHubHelper.js: -------------------------------------------------------------------------------- 1 | import { 2 | getDateFromDescriptionYaml, 3 | getNumberFromDescriptionYaml, 4 | removeFirstSharp, 5 | replacePropertyInDescriptionString, 6 | getDependonFromDescriptionYaml, 7 | } from '../Common/Parser.js'; 8 | import { 9 | calculateDueDate, 10 | getGanttStartDate, 11 | getGanttDueDate, 12 | getGanttDuration, 13 | orgRound, 14 | adjustDateString, 15 | getGanttUpdateDate, 16 | isValidVariable, 17 | } from '../Common/CommonHelper.js'; 18 | 19 | const getGitHubAssignee = (issue_info) => { 20 | if (issue_info.assignee !== null) { 21 | return issue_info.assignee.login; 22 | } 23 | return ''; 24 | }; 25 | 26 | export const generateGanttTaskFromGitHub = (description, issue_info) => { 27 | const start_date = getDateFromDescriptionYaml(description, 'start_date'); 28 | const due_date = getDateFromDescriptionYaml(description, 'due_date'); 29 | 30 | const gantt_task = { 31 | id: '#' + issue_info.number, 32 | text: issue_info.title, 33 | start_date: getGanttStartDate(start_date, due_date, issue_info.created_at), 34 | due_date: getGanttDueDate(start_date, due_date, issue_info.created_at), 35 | duration: getGanttDuration(start_date, due_date, issue_info.created_at), 36 | progress: getNumberFromDescriptionYaml(description, 'progress'), 37 | assignee: getGitHubAssignee(issue_info), 38 | parent: '#' + getNumberFromDescriptionYaml(description, 'parent'), 39 | _parent: '#' + getNumberFromDescriptionYaml(description, 'parent'), 40 | description: description, 41 | update: getGanttUpdateDate(issue_info.created_at, issue_info.updated_at), 42 | }; 43 | 44 | let links = []; 45 | const link = generateLinkFromGitHub(description, issue_info); 46 | if (typeof link != "undefined") { 47 | for (let i = 0; i < link.length; i++) { 48 | let prelink = { 49 | type: link[i].type, 50 | target: link[i].target, 51 | source: link[i].source, 52 | } 53 | links.push(prelink); 54 | } 55 | } 56 | gantt_task.links = links; 57 | 58 | return gantt_task; 59 | }; 60 | 61 | export const generateLinkFromGitHub = (description, issue_info) => { 62 | const link = []; 63 | let dependon = []; 64 | dependon = getDependonFromDescriptionYaml(description, 'dependon'); 65 | if (isValidVariable(dependon)) { 66 | //let data = []; 67 | for (let i = 0; i < dependon.length; i++) { 68 | let data = []; 69 | data.type = '0'; 70 | data.target = '#' + issue_info.number; 71 | data.source = '#' + dependon[i]; 72 | link.push(data); 73 | } 74 | return link; 75 | } 76 | }; 77 | 78 | export const updateGitHubDescriptionStringFromGanttTask = ( 79 | description, 80 | gantt_task 81 | ) => { 82 | const start_date_str = adjustDateString(gantt_task.start_date) 83 | .replace(/\-/g, '/'); 84 | const due_date_str = calculateDueDate( 85 | start_date_str, 86 | gantt_task.duration 87 | ).replace(/\-/g, '/'); 88 | const task = { 89 | start_date: start_date_str, 90 | due_date: due_date_str, 91 | progress: orgRound(gantt_task.progress, 0.01), 92 | }; 93 | task.parent = parseInt(removeFirstSharp(gantt_task.parent)); 94 | if ('dependon' in gantt_task) { 95 | task.dependon = gantt_task.dependon; 96 | } 97 | description = replacePropertyInDescriptionString(description, task); 98 | return description; 99 | }; 100 | -------------------------------------------------------------------------------- /src/functions/GitHub/GitHubHelper.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | generateGanttTaskFromGitHub, 3 | updateGitHubDescriptionStringFromGanttTask, 4 | } from './GitHubHelper'; 5 | 6 | const description = `\`\`\`yaml 7 | start_date: 2021/2/5 8 | due_date: 2021/2/5 9 | progress: 0.5 10 | parent: 5 11 | \`\`\` 12 | 13 | ## 概要 14 | issueの内容 15 | `; 16 | 17 | const issue_info = { 18 | number: 36, 19 | title: 'テストissueのタイトル', 20 | assignee: { login: 'satoshi' }, 21 | description: description, 22 | updated_at:'2021/2/5', 23 | }; 24 | 25 | const gantt_task = { 26 | id: '#36', 27 | text: 'テストissueのタイトル', 28 | start_date: '2021/2/5', 29 | due_date: new Date('2021/2/5'), 30 | duration: 1, 31 | progress: 0.5, 32 | assignee: 'satoshi', 33 | parent: '#5', 34 | _parent: '#5', 35 | description: description, 36 | update:'2021/2/5', 37 | links: [], 38 | }; 39 | 40 | describe('generateGanttTaskFromGitHub', () => { 41 | test('true', () => { 42 | expect(generateGanttTaskFromGitHub(description, issue_info)).toEqual( 43 | gantt_task 44 | ); 45 | }); 46 | }); 47 | 48 | describe('updateGitHubDescriptionStringFromGanttTask', () => { 49 | test('true', () => { 50 | expect( 51 | updateGitHubDescriptionStringFromGanttTask(description, gantt_task) 52 | ).toEqual(description); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/functions/GitHub/GitHubURLHelper.js: -------------------------------------------------------------------------------- 1 | import { 2 | isValidVariable, 3 | isValidIDName, 4 | isValidURL, 5 | isNumber, 6 | } from '../Common/CommonHelper.js'; 7 | import { removeFirstSharp } from '../Common/Parser.js'; 8 | 9 | const GitHubAPIURL = 'https://api.github.com/repos/'; 10 | const GitHubURL = 'https://github.com/'; 11 | 12 | export const isGitHubURL = (git_url) => { 13 | if (!isValidURL(git_url)) { 14 | return false; 15 | } 16 | if (git_url.split('/').length < 5) { 17 | return false; 18 | } 19 | return /github\.com/.test(git_url); 20 | }; 21 | 22 | export const getGitHubNameSpaceFromGitURL = (git_url) => { 23 | if (!isGitHubURL(git_url)) { 24 | return null; 25 | } 26 | const split_git_url = git_url.split('/'); 27 | if (split_git_url.length >= 5) { 28 | return split_git_url[3]; 29 | } 30 | return null; 31 | }; 32 | 33 | export const getGitHubProjectFromGitURL = (git_url) => { 34 | if (!isGitHubURL(git_url)) { 35 | return null; 36 | } 37 | const split_git_url = git_url.split('/'); 38 | if (split_git_url.length >= 5) { 39 | return split_git_url[4]; 40 | } 41 | return null; 42 | }; 43 | 44 | export const getGitHubAPIURLIssue = (git_url) => { 45 | if (!isGitHubURL(git_url)) { 46 | return null; 47 | } 48 | return ( 49 | GitHubAPIURL + 50 | getGitHubNameSpaceFromGitURL(git_url) + 51 | '/' + 52 | getGitHubProjectFromGitURL(git_url) + 53 | '/issues' 54 | ); 55 | }; 56 | 57 | export const getGitHubAPIURLIssuebyNumber = (git_url, number) => { 58 | if (!isGitHubURL(git_url) || !isValidVariable(number)) { 59 | return null; 60 | } 61 | return ( 62 | GitHubAPIURL + 63 | getGitHubNameSpaceFromGitURL(git_url) + 64 | '/' + 65 | getGitHubProjectFromGitURL(git_url) + 66 | '/issues/' + 67 | number 68 | ); 69 | }; 70 | 71 | export const getGitHubAPIURLIssueFilterd = (git_url, labels, assignee) => { 72 | if ( 73 | !isGitHubURL(git_url) || 74 | !isValidVariable(labels) || 75 | !isValidVariable(assignee) 76 | ) { 77 | return null; 78 | } 79 | let url_query_str = '?'; 80 | url_query_str += 'labels='; 81 | labels.map((label) => { 82 | if (isValidIDName(label)) { 83 | url_query_str += label.name + ','; 84 | } 85 | }); 86 | 87 | if (isValidIDName(assignee)) { 88 | if (assignee.name !== '') { 89 | url_query_str += '&assignee=' + assignee.name; 90 | } 91 | } 92 | return ( 93 | GitHubAPIURL + 94 | getGitHubNameSpaceFromGitURL(git_url) + 95 | '/' + 96 | getGitHubProjectFromGitURL(git_url) + 97 | '/issues' + 98 | url_query_str 99 | ); 100 | }; 101 | 102 | export const getGitHubAPIURLLabel = (git_url) => { 103 | if (!isGitHubURL(git_url)) { 104 | return null; 105 | } 106 | return ( 107 | GitHubAPIURL + 108 | getGitHubNameSpaceFromGitURL(git_url) + 109 | '/' + 110 | getGitHubProjectFromGitURL(git_url) + 111 | '/labels' 112 | ); 113 | }; 114 | 115 | export const getGitHubAPIURLCollaborators = (git_url) => { 116 | if (!isGitHubURL(git_url)) { 117 | return null; 118 | } 119 | return ( 120 | GitHubAPIURL + 121 | getGitHubNameSpaceFromGitURL(git_url) + 122 | '/' + 123 | getGitHubProjectFromGitURL(git_url) + 124 | '/collaborators' 125 | ); 126 | }; 127 | 128 | export const getGitHubURLIssuebyNumber = (git_url, number) => { 129 | if (!isGitHubURL(git_url) || !isValidVariable(number)) { 130 | return null; 131 | } 132 | if (!isNumber(number)) { 133 | number = removeFirstSharp(number); 134 | } 135 | if (number <= 0) { 136 | return null; 137 | } 138 | return ( 139 | GitHubURL + 140 | getGitHubNameSpaceFromGitURL(git_url) + 141 | '/' + 142 | getGitHubProjectFromGitURL(git_url) + 143 | '/issues/' + 144 | number 145 | ); 146 | }; 147 | 148 | export const getGitHubURLNewIssueWithTemplate = (git_url) => { 149 | if (!isGitHubURL(git_url)) { 150 | return null; 151 | } 152 | return ( 153 | GitHubURL + 154 | getGitHubNameSpaceFromGitURL(git_url) + 155 | '/' + 156 | getGitHubProjectFromGitURL(git_url) + 157 | '/issues/new?assignees=&labels=&title=&body=' 158 | ); 159 | }; 160 | -------------------------------------------------------------------------------- /src/functions/GitHub/GitHubURLHelper.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | isGitHubURL, 3 | getGitHubNameSpaceFromGitURL, 4 | getGitHubProjectFromGitURL, 5 | getGitHubAPIURLIssue, 6 | getGitHubAPIURLIssuebyNumber, 7 | getGitHubAPIURLIssueFilterd, 8 | getGitHubAPIURLLabel, 9 | getGitHubAPIURLCollaborators, 10 | getGitHubURLIssuebyNumber, 11 | getGitHubURLNewIssueWithTemplate, 12 | } from './GitHubURLHelper'; 13 | 14 | describe('isGitHubURL', () => { 15 | test('true', () => { 16 | expect( 17 | isGitHubURL('https://github.com/lamact/react-issue-ganttchart/') 18 | ).toBe(true); 19 | }); 20 | 21 | test('true', () => { 22 | expect( 23 | isGitHubURL('https://github.com/lamact/react-issue-ganttchart') 24 | ).toBe(true); 25 | }); 26 | 27 | test('true', () => { 28 | expect(isGitHubURL('https://github.com/xxxxxx/yyyyyyyy/zzzzzzzzz')).toBe( 29 | true 30 | ); 31 | }); 32 | 33 | test('false', () => { 34 | expect( 35 | isGitHubURL('https://gitlab.com/lamact/react-issue-ganttchart/') 36 | ).toBe(false); 37 | }); 38 | 39 | test('false', () => { 40 | expect(isGitHubURL('github.com/lamact/react-issue-ganttchart/')).toBe( 41 | false 42 | ); 43 | }); 44 | 45 | test('false', () => { 46 | expect(isGitHubURL('https://github.com/')).toBe(false); 47 | }); 48 | }); 49 | 50 | describe('getGitHubNameSpaceFromGitURL', () => { 51 | test('true', () => { 52 | expect( 53 | getGitHubNameSpaceFromGitURL( 54 | 'https://github.com/lamact/react-issue-ganttchart/' 55 | ) 56 | ).toBe('lamact'); 57 | }); 58 | 59 | test('true', () => { 60 | expect( 61 | getGitHubNameSpaceFromGitURL( 62 | 'https://github.com/lamact/react-issue-ganttchart' 63 | ) 64 | ).toBe('lamact'); 65 | }); 66 | 67 | test('true', () => { 68 | expect( 69 | getGitHubNameSpaceFromGitURL( 70 | 'https://github.com/xxxxxx/yyyyyyyy/zzzzzzzzz' 71 | ) 72 | ).toBe('xxxxxx'); 73 | }); 74 | 75 | test('null', () => { 76 | expect( 77 | getGitHubNameSpaceFromGitURL( 78 | 'https://gitlab.com/lamact/react-issue-ganttchart/' 79 | ) 80 | ).toBe(null); 81 | }); 82 | 83 | test('true', () => { 84 | expect( 85 | getGitHubNameSpaceFromGitURL('github.com/lamact/react-issue-ganttchart/') 86 | ).toBe(null); 87 | }); 88 | 89 | test('false', () => { 90 | expect(getGitHubNameSpaceFromGitURL('https://github.com/')).toBe(null); 91 | }); 92 | }); 93 | 94 | describe('getGitHubProjectFromGitURL', () => { 95 | test('true', () => { 96 | expect( 97 | getGitHubProjectFromGitURL( 98 | 'https://github.com/lamact/react-issue-ganttchart/' 99 | ) 100 | ).toBe('react-issue-ganttchart'); 101 | }); 102 | 103 | test('true', () => { 104 | expect( 105 | getGitHubProjectFromGitURL( 106 | 'https://github.com/lamact/react-issue-ganttchart' 107 | ) 108 | ).toBe('react-issue-ganttchart'); 109 | }); 110 | 111 | test('true', () => { 112 | expect( 113 | getGitHubProjectFromGitURL('https://github.com/xxxxxx/yyyyyyyy/zzzzzzzzz') 114 | ).toBe('yyyyyyyy'); 115 | }); 116 | 117 | test('null', () => { 118 | expect( 119 | getGitHubProjectFromGitURL( 120 | 'https://gitlab.com/lamact/react-issue-ganttchart/' 121 | ) 122 | ).toBe(null); 123 | }); 124 | 125 | test('false', () => { 126 | expect(getGitHubProjectFromGitURL('https://github.com/')).toBe(null); 127 | }); 128 | }); 129 | 130 | describe('getGitHubAPIURLIssue', () => { 131 | test('true', () => { 132 | expect( 133 | getGitHubAPIURLIssue('https://github.com/lamact/react-issue-ganttchart/') 134 | ).toBe('https://api.github.com/repos/lamact/react-issue-ganttchart/issues'); 135 | }); 136 | test('false', () => { 137 | expect( 138 | getGitHubAPIURLIssue('https://github.com/lamact/react-issue-ganttchart/') 139 | ).toBe('https://api.github.com/repos/lamact/react-issue-ganttchart/issues'); 140 | }); 141 | test('null', () => { 142 | expect( 143 | getGitHubAPIURLIssue('https://github.com/lamact/react-issue-ganttchart/') 144 | ).toBe('https://api.github.com/repos/lamact/react-issue-ganttchart/issues'); 145 | }); 146 | }); 147 | 148 | describe('getGitHubAPIURLIssuebyNumber', () => { 149 | test('true', () => { 150 | expect( 151 | getGitHubAPIURLIssuebyNumber( 152 | 'https://github.com/lamact/react-issue-ganttchart/', 153 | 10 154 | ) 155 | ).toBe( 156 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/issues/10' 157 | ); 158 | }); 159 | test('true', () => { 160 | expect( 161 | getGitHubAPIURLIssuebyNumber( 162 | 'https://github.com/lamact/react-issue-ganttchart/aaaaa', 163 | 10000 164 | ) 165 | ).toBe( 166 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/issues/10000' 167 | ); 168 | }); 169 | test('null', () => { 170 | expect( 171 | getGitHubAPIURLIssuebyNumber( 172 | 'https://github.com/lamact/react-issue-ganttchart/', 173 | null 174 | ) 175 | ).toBe(null); 176 | }); 177 | }); 178 | 179 | describe('getGitHubAPIURLIssueFilterd', () => { 180 | test('true', () => { 181 | expect( 182 | getGitHubAPIURLIssueFilterd( 183 | 'https://github.com/lamact/react-issue-ganttchart/', 184 | [{ id: 1, name: 'todo' }], 185 | { id: 1, name: 'satoshi' } 186 | ) 187 | ).toBe( 188 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/issues?labels=todo,&assignee=satoshi' 189 | ); 190 | }); 191 | test('true', () => { 192 | expect( 193 | getGitHubAPIURLIssueFilterd( 194 | 'https://github.com/lamact/react-issue-ganttchart/', 195 | [ 196 | { id: 1, name: 'todo' }, 197 | { id: 2, name: 'doing' }, 198 | { id: 3, name: 'done' }, 199 | ], 200 | { id: 1, name: 'satoshi' } 201 | ) 202 | ).toBe( 203 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/issues?labels=todo,doing,done,&assignee=satoshi' 204 | ); 205 | }); 206 | test('true', () => { 207 | expect( 208 | getGitHubAPIURLIssueFilterd( 209 | 'https://github.com/lamact/react-issue-ganttchart/', 210 | [{ id: 1, name: 'todo' }], 211 | [] 212 | ) 213 | ).toBe( 214 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/issues?labels=todo,' 215 | ); 216 | }); 217 | test('true', () => { 218 | expect( 219 | getGitHubAPIURLIssueFilterd( 220 | 'https://github.com/lamact/react-issue-ganttchart/', 221 | [], 222 | { id: 1, name: 'satoshi' } 223 | ) 224 | ).toBe( 225 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/issues?labels=&assignee=satoshi' 226 | ); 227 | }); 228 | test('true', () => { 229 | expect( 230 | getGitHubAPIURLIssueFilterd( 231 | 'https://github.com/lamact/react-issue-ganttchart/', 232 | [{ id: 1, name: 'todo' }], 233 | [] 234 | ) 235 | ).toBe( 236 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/issues?labels=todo,' 237 | ); 238 | }); 239 | test('null', () => { 240 | expect( 241 | getGitHubAPIURLIssueFilterd( 242 | 'https://github.com/lamact/react-issue-ganttchart/' 243 | ) 244 | ).toBe(null); 245 | }); 246 | }); 247 | 248 | describe('getGitHubAPIURLLabel', () => { 249 | test('true', () => { 250 | expect( 251 | getGitHubAPIURLLabel('https://github.com/lamact/react-issue-ganttchart/') 252 | ).toBe('https://api.github.com/repos/lamact/react-issue-ganttchart/labels'); 253 | }); 254 | test('false', () => { 255 | expect( 256 | getGitHubAPIURLLabel('https://github.com/lamact/react-issue-ganttchart/') 257 | ).toBe('https://api.github.com/repos/lamact/react-issue-ganttchart/labels'); 258 | }); 259 | test('null', () => { 260 | expect( 261 | getGitHubAPIURLLabel('github.com/lamact/react-issue-ganttchart/') 262 | ).toBe(null); 263 | }); 264 | }); 265 | 266 | describe('getGitHubAPIURLCollaborators', () => { 267 | test('true', () => { 268 | expect( 269 | getGitHubAPIURLCollaborators( 270 | 'https://github.com/lamact/react-issue-ganttchart/' 271 | ) 272 | ).toBe( 273 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/collaborators' 274 | ); 275 | }); 276 | test('false', () => { 277 | expect( 278 | getGitHubAPIURLCollaborators( 279 | 'https://github.com/lamact/react-issue-ganttchart/' 280 | ) 281 | ).toBe( 282 | 'https://api.github.com/repos/lamact/react-issue-ganttchart/collaborators' 283 | ); 284 | }); 285 | test('null', () => { 286 | expect(getGitHubAPIURLCollaborators('github.com/lamact')).toBe(null); 287 | }); 288 | }); 289 | 290 | describe('getGitHubURLIssuebyNumber', () => { 291 | test('true', () => { 292 | expect( 293 | getGitHubURLIssuebyNumber( 294 | 'https://github.com/lamact/react-issue-ganttchart/', 295 | 15 296 | ) 297 | ).toBe('https://github.com/lamact/react-issue-ganttchart/issues/15'); 298 | }); 299 | test('null', () => { 300 | expect( 301 | getGitHubURLIssuebyNumber( 302 | 'https://github.com/lamact/react-issue-ganttchart/', 303 | '#12' 304 | ) 305 | ).toBe("https://github.com/lamact/react-issue-ganttchart/issues/12"); 306 | }); 307 | test('null', () => { 308 | expect( 309 | getGitHubURLIssuebyNumber( 310 | 'https://github.com/lamact/react-issue-ganttchart/', 311 | 0 312 | ) 313 | ).toBe(null); 314 | }); 315 | test('null', () => { 316 | expect( 317 | getGitHubURLIssuebyNumber('github.com/lamact/react-issue-ganttchart/', 2) 318 | ).toBe(null); 319 | }); 320 | }); 321 | 322 | describe('getGitHubURLNewIssueWithTemplate', () => { 323 | test('true', () => { 324 | expect( 325 | getGitHubURLNewIssueWithTemplate( 326 | 'https://github.com/lamact/react-issue-ganttchart/' 327 | ) 328 | ).toBe( 329 | 'https://github.com/lamact/react-issue-ganttchart/issues/new?assignees=&labels=&title=&body=' 330 | ); 331 | }); 332 | test('null', () => { 333 | expect( 334 | getGitHubURLIssuebyNumber('github.com/lamact/react-issue-ganttchart/') 335 | ).toBe(null); 336 | }); 337 | }); 338 | -------------------------------------------------------------------------------- /src/functions/GitLab/GitLabAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { 3 | getGitLabAPIURLIssueFilterd, 4 | getGitLabAPIURLLabel, 5 | getGitLabAPIURLMember, 6 | getGitabAPIURLIssuebyNumber, 7 | getGitLabURLIssuebyNumber, 8 | getGitLabURLNewIssueWithTemplate, 9 | } from './GitLabURLHelper.js'; 10 | import { 11 | generateGanttTaskFromGitLab, 12 | updateGitLabDescriptionStringFromGanttTask, 13 | } from './GitLabHelper.js'; 14 | import { 15 | adjustDateString, 16 | ArrangeGanttTaskToGeneratedGanttTaskForCompare, 17 | calculateDueDate, 18 | date2string, 19 | isEqualGanntTask, 20 | } from '../Common/CommonHelper.js'; 21 | import { 22 | removeFirstSharp, 23 | replacePropertyInDescriptionString, 24 | } from '../Common/Parser.js'; 25 | 26 | export const getGitLabIssuesFromAPI = async ( 27 | git_url, 28 | token, 29 | selected_labels, 30 | assignee 31 | ) => { 32 | return axios 33 | .get(getGitLabAPIURLIssueFilterd(git_url, token, selected_labels, assignee)) 34 | .then((res) => { 35 | let data = []; 36 | res.data.map((issue_info) => { 37 | data.push(generateGanttTaskFromGitLab(issue_info)); 38 | }); 39 | return data; 40 | }) 41 | .catch((err) => { 42 | console.error(err); 43 | return Promise.reject(err); 44 | }); 45 | }; 46 | 47 | export const setGitLabLabelListOfRepoFromAPI = async (git_url, token) => { 48 | return axios 49 | .get(getGitLabAPIURLLabel(git_url, token)) 50 | .then((res) => { 51 | let labels = []; 52 | res.data.map((lebel_info) => { 53 | labels.push(lebel_info); 54 | return null; 55 | }); 56 | return labels; 57 | }) 58 | .catch((err) => { 59 | console.error(err); 60 | return Promise.reject(err); 61 | }); 62 | }; 63 | 64 | export const setGitLabMemberListOfRepoFromAPI = async (git_url, token) => { 65 | return axios 66 | .get(getGitLabAPIURLMember(git_url, token)) 67 | .then((res) => { 68 | let members = []; 69 | res.data.map((info) => { 70 | members.push({ id: info.id, name: info.name }); 71 | return null; 72 | }); 73 | return members; 74 | }) 75 | .catch((err) => { 76 | console.error(err); 77 | return Promise.reject(err); 78 | }); 79 | }; 80 | 81 | export const updateGitLabIssueFromGanttTask = ( 82 | gantt_task, 83 | token, 84 | gantt, 85 | git_url 86 | ) => { 87 | return axios 88 | .get( 89 | getGitabAPIURLIssuebyNumber( 90 | git_url, 91 | token, 92 | removeFirstSharp(gantt_task.id) 93 | ) 94 | ) 95 | .then((res) => { 96 | const issue_info = res.data; 97 | // Update if different from existing parameters 98 | if (!isEqualGanntTask( 99 | ArrangeGanttTaskToGeneratedGanttTaskForCompare(gantt_task), 100 | generateGanttTaskFromGitLab(issue_info) 101 | )) { 102 | if ( 103 | parseInt(issue_info.iid) === parseInt(removeFirstSharp(gantt_task.id)) 104 | ) { 105 | let description = updateGitLabDescriptionStringFromGanttTask( 106 | issue_info.description, 107 | gantt_task 108 | ); 109 | if (description == null) { 110 | gantt.message({ 111 | text: 'failed update issue. ' + gantt_task.text, 112 | type: 'error', 113 | }); 114 | } else { 115 | description = encodeURIComponent(description); 116 | const start_date_str = adjustDateString(gantt_task.start_date); 117 | const due_date_str = calculateDueDate( 118 | start_date_str, 119 | gantt_task.duration 120 | ); 121 | const put_url = 122 | getGitabAPIURLIssuebyNumber( 123 | git_url, 124 | token, 125 | removeFirstSharp(gantt_task.id) 126 | ) + 127 | '&description=' + 128 | description + 129 | '&due_date=' + 130 | due_date_str; 131 | return axios 132 | .put(put_url) 133 | .then((res) => { 134 | gantt.message({ 135 | text: 'success update issue. ' + gantt_task.id, 136 | }); 137 | }) 138 | .catch((err) => { 139 | console.error(err); 140 | return Promise.reject(err); 141 | }); 142 | } 143 | } 144 | } 145 | }) 146 | .catch((err) => { 147 | gantt.message({ 148 | text: 'failed get GitLab issue. check your token.', 149 | type: 'error', 150 | }); 151 | }); 152 | }; 153 | 154 | export const openGitLabIssueAtBrowser = (id, git_url) => { 155 | window.open( 156 | getGitLabURLIssuebyNumber(git_url, removeFirstSharp(id)), 157 | '_blank' 158 | ); 159 | }; 160 | 161 | export const openGitLabNewIssueAtBrowser = (gantt_task, git_url) => { 162 | const start_date_str = date2string(new Date()); 163 | if (gantt_task.parent == null) { 164 | gantt_task.parent = 0; 165 | } 166 | const task = { 167 | start_date: start_date_str, 168 | progress: 0.1, 169 | parent: parseInt(removeFirstSharp(gantt_task.parent)), 170 | }; 171 | let body = replacePropertyInDescriptionString('', task); 172 | body = encodeURIComponent(body); 173 | window.open(getGitLabURLNewIssueWithTemplate(git_url) + body, '_blank'); 174 | }; 175 | -------------------------------------------------------------------------------- /src/functions/GitLab/GitLabHelper.js: -------------------------------------------------------------------------------- 1 | import { 2 | removeFirstSharp, 3 | getDateFromDescriptionYaml, 4 | getNumberFromDescriptionYaml, 5 | replacePropertyInDescriptionString, 6 | getDependonFromDescriptionYaml, 7 | } from '../Common/Parser.js'; 8 | import { 9 | getGanttStartDate, 10 | getGanttDueDate, 11 | getGanttDuration, 12 | orgRound, 13 | adjustDateString, 14 | isValidVariable, 15 | getGanttUpdateDate, 16 | } from '../Common/CommonHelper.js'; 17 | 18 | const getGitLabAssignee = (issue_info) => { 19 | if (isValidVariable(issue_info) && 'assignee' in issue_info) { 20 | if (isValidVariable(issue_info.assignee) && 'name' in issue_info.assignee) { 21 | return issue_info.assignee.name; 22 | } 23 | } 24 | return ''; 25 | }; 26 | 27 | export const generateGanttTaskFromGitLab = (issue_info) => { 28 | const start_date = getDateFromDescriptionYaml( 29 | issue_info.description, 30 | 'start_date' 31 | ); 32 | const due_date = adjustDateString(issue_info.due_date); 33 | var parent = getNumberFromDescriptionYaml(issue_info.description, 'parent'); 34 | if (parent !== null) { 35 | parent = '#' + parent; 36 | } 37 | var gantt_task = { 38 | id: '#' + issue_info.iid, 39 | text: issue_info.title, 40 | start_date: getGanttStartDate(start_date, due_date, issue_info.created_at), 41 | due_date: getGanttDueDate(start_date, due_date, issue_info.created_at), 42 | duration: getGanttDuration(start_date, due_date, issue_info.created_at), 43 | progress: getNumberFromDescriptionYaml(issue_info.description, 'progress'), 44 | assignee: getGitLabAssignee(issue_info), 45 | description: issue_info.description, 46 | update: getGanttUpdateDate(issue_info.created_at, issue_info.updated_at), 47 | parent: parent, 48 | _parent: parent, 49 | }; 50 | 51 | let links = []; 52 | const link = generateLinkFromGitLab(issue_info); 53 | if (typeof link != "undefined") { 54 | for (let i = 0; i < link.length; i++) { 55 | let prelink = { 56 | type: link[i].type, 57 | target: link[i].target, 58 | source: link[i].source, 59 | } 60 | links.push(prelink); 61 | } 62 | } 63 | gantt_task.links = links; 64 | return gantt_task; 65 | }; 66 | 67 | export const generateLinkFromGitLab = (issue_info) => { 68 | const link = []; 69 | let dependon = []; 70 | dependon = getDependonFromDescriptionYaml(issue_info.description, 'dependon'); 71 | if (dependon != null) { 72 | //let data = []; 73 | for (let i = 0; i < dependon.length; i++) { 74 | let data = []; 75 | data.type = '0'; 76 | data.target = '#' + issue_info.iid; 77 | data.source = '#' + dependon[i]; 78 | link.push(data); 79 | } 80 | return link; 81 | } 82 | }; 83 | 84 | export const updateGitLabDescriptionStringFromGanttTask = ( 85 | description, 86 | gantt_task 87 | ) => { 88 | const start_date_str = adjustDateString(gantt_task.start_date).replace( 89 | /\-/g, 90 | '/' 91 | ); 92 | const task = { 93 | start_date: start_date_str, 94 | progress: orgRound(gantt_task.progress, 0.01), 95 | }; 96 | if ('parent' in gantt_task && gantt_task.parent != null) { 97 | task.parent = parseInt(removeFirstSharp(gantt_task.parent)); 98 | } 99 | if ('dependon' in gantt_task) { 100 | task.dependon = gantt_task.dependon; 101 | } 102 | return replacePropertyInDescriptionString(description, task); 103 | }; 104 | -------------------------------------------------------------------------------- /src/functions/GitLab/GitLabHelper.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | generateGanttTaskFromGitLab, 3 | updateGitLabDescriptionStringFromGanttTask, 4 | } from './GitLabHelper'; 5 | 6 | const description = `\`\`\`yaml 7 | start_date: 2021/2/5 8 | progress: 0.5 9 | parent: 5 10 | \`\`\` 11 | 12 | ## 概要 13 | issueの内容 14 | `; 15 | 16 | const issue_info = { 17 | iid: 36, 18 | title: 'テストissueのタイトル', 19 | assignee: { name: 'satoshi' }, 20 | due_date: new Date('2021/2/5'), 21 | description: description, 22 | updated_at:new Date('2021/2/5'), 23 | }; 24 | 25 | const gantt_task = { 26 | id: '#36', 27 | links: [], 28 | text: 'テストissueのタイトル', 29 | start_date: '2021/2/5', 30 | due_date: new Date('2021/2/5'), 31 | duration: 1, 32 | progress: 0.5, 33 | assignee: 'satoshi', 34 | parent: '#5', 35 | _parent: '#5', 36 | description: description, 37 | update:'2021/2/5', 38 | }; 39 | 40 | describe('have parent', () => { 41 | test('true', () => { 42 | expect(generateGanttTaskFromGitLab(issue_info)).toEqual(gantt_task); 43 | }); 44 | test('true', () => { 45 | expect( 46 | updateGitLabDescriptionStringFromGanttTask(description, gantt_task) 47 | ).toEqual(description); 48 | }); 49 | }); 50 | 51 | const description_dont_have_parent = `\`\`\`yaml 52 | start_date: 2021/2/5 53 | progress: 0.5 54 | \`\`\` 55 | 56 | ## 概要 57 | issueの内容 58 | `; 59 | 60 | const issue_info_dont_have_parent = { 61 | iid: 36, 62 | title: 'テストissueのタイトル', 63 | assignee: { name: 'satoshi' }, 64 | due_date: new Date('2021/2/5'), 65 | description: description_dont_have_parent, 66 | updated_at:new Date('2021/2/5'), 67 | }; 68 | 69 | const gantt_task_dont_have_parent = { 70 | id: '#36', 71 | text: 'テストissueのタイトル', 72 | start_date: '2021/2/5', 73 | due_date: new Date('2021/2/5'), 74 | duration: 1, 75 | progress: 0.5, 76 | assignee: 'satoshi', 77 | description: description_dont_have_parent, 78 | update:'2021/2/5', 79 | links:[], 80 | parent: null, 81 | _parent: null 82 | }; 83 | 84 | describe('have parent', () => { 85 | test('true', () => { 86 | expect(generateGanttTaskFromGitLab(issue_info_dont_have_parent)).toEqual( 87 | gantt_task_dont_have_parent 88 | ); 89 | }); 90 | test('true', () => { 91 | expect( 92 | updateGitLabDescriptionStringFromGanttTask( 93 | description_dont_have_parent, 94 | gantt_task_dont_have_parent 95 | ) 96 | ).toEqual(description_dont_have_parent); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/functions/GitLab/GitLabURLHelper.js: -------------------------------------------------------------------------------- 1 | import { 2 | isValidVariable, 3 | isValidIDName, 4 | isValidURL, 5 | } from '../Common/CommonHelper.js'; 6 | import { 7 | removeLastSlash, 8 | removeLastSpace, 9 | } from '../Common/Parser.js'; 10 | import { isGitHubURL } from '../GitHub/GitHubURLHelper.js'; 11 | 12 | export const isGitLabURL = (git_url) => { 13 | if (!isValidURL(git_url)) { 14 | return false; 15 | } 16 | if (git_url.split('/').length < 5) { 17 | return false; 18 | } 19 | return /gitlab\.com/.test(git_url); 20 | }; 21 | 22 | export const getSelfHostingGitLabDomain = (git_url) => { 23 | if (isGitHubURL(git_url)) { 24 | return null; 25 | } 26 | if (!isValidURL(git_url)) { 27 | return null; 28 | } 29 | const split_git_url = git_url.split('/'); 30 | if (split_git_url.length >= 5) { 31 | return split_git_url[2]; 32 | } 33 | return null; 34 | }; 35 | 36 | export const getGitLabDomain = (git_url) => { 37 | if (!isValidURL(git_url)) { 38 | return null; 39 | } 40 | let gitlab_domain = null; 41 | const self_hosting_gitlab_domain = getSelfHostingGitLabDomain(git_url); 42 | if (self_hosting_gitlab_domain !== null) { 43 | gitlab_domain = git_url.substr(0, git_url.indexOf(':')) + '://' + self_hosting_gitlab_domain + '/'; 44 | } 45 | return gitlab_domain; 46 | }; 47 | 48 | export const getGitLabURL = (git_url) => { 49 | if (!isValidURL(git_url)) { 50 | return null; 51 | } 52 | return getGitLabDomain(git_url); 53 | }; 54 | 55 | export const getGitLabAPIURL = (git_url) => { 56 | if (!isValidURL(git_url)) { 57 | return null; 58 | } 59 | return getGitLabDomain(git_url) + 'api/v4/'; 60 | }; 61 | 62 | export const getGitLabNameSpaceFromGitURL = (git_url) => { 63 | if (!isValidURL(git_url)) { 64 | return null; 65 | } 66 | git_url = removeLastSlash(removeLastSpace(git_url)); 67 | const split_git_url = git_url.split('/'); 68 | if (split_git_url.length == 5) { 69 | return split_git_url[3]; 70 | } 71 | if (split_git_url.length >= 5) { 72 | return split_git_url.slice(3, split_git_url.length - 1).join('%2F'); 73 | } 74 | return null; 75 | }; 76 | 77 | export const getGitLabNameSpaceFromGitURLsplitSlash = (git_url) => { 78 | if (!isValidURL(git_url)) { 79 | return null; 80 | } 81 | git_url = removeLastSlash(removeLastSpace(git_url)); 82 | const split_git_url = git_url.split('/'); 83 | if (split_git_url.length == 5) { 84 | return split_git_url[3]; 85 | } 86 | if (split_git_url.length >= 5) { 87 | return split_git_url.slice(3, split_git_url.length - 1).join('/'); 88 | } 89 | return null; 90 | }; 91 | 92 | export const getGitLabProjectFromGitURL = (git_url) => { 93 | if (!isValidURL(git_url)) { 94 | return null; 95 | } 96 | git_url = removeLastSlash(removeLastSpace(git_url)); 97 | const split_git_url = git_url.split('/'); 98 | if (split_git_url.length == 5) { 99 | return split_git_url[4]; 100 | } 101 | if (split_git_url.length >= 5) { 102 | return split_git_url[split_git_url.length - 1]; 103 | } 104 | return null; 105 | }; 106 | 107 | export const postFixToken = (token) => { 108 | let post_fix_str = '?'; 109 | if ( 110 | isValidVariable(token) && 111 | token !== 'Tokens that have not yet been entered' 112 | ) { 113 | post_fix_str += 'access_token=' + token; 114 | } 115 | return post_fix_str; 116 | }; 117 | 118 | export const getGitLabAPIURLIssueFilterd = ( 119 | git_url, 120 | token, 121 | labels, 122 | assignee 123 | ) => { 124 | if (!isValidURL(git_url)) { 125 | return null; 126 | } 127 | if (!isValidVariable(token)) { 128 | return null; 129 | } 130 | if (!isValidVariable(labels)) { 131 | return null; 132 | } 133 | if (!isValidIDName(assignee)) { 134 | return null; 135 | } 136 | let post_fix_str = postFixToken(token); 137 | if (isValidVariable(labels)) { 138 | post_fix_str += '&labels='; 139 | labels.map((label) => { 140 | if (isValidIDName(label)) { 141 | post_fix_str += label.name + ','; 142 | } 143 | return null; 144 | }); 145 | } 146 | if (isValidIDName(assignee)) { 147 | if (assignee.name !== '') { 148 | post_fix_str += '&assignee_id=' + assignee.id; 149 | } 150 | } 151 | post_fix_str += '&per_page=100&state=opened'; 152 | return ( 153 | getGitLabAPIURL(git_url) + 154 | 'projects/' + 155 | getGitLabNameSpaceFromGitURL(git_url) + 156 | '%2F' + 157 | getGitLabProjectFromGitURL(git_url) + 158 | '/issues' + 159 | post_fix_str 160 | ); 161 | }; 162 | 163 | export const getGitabAPIURLIssuebyNumber = (git_url, token, number) => { 164 | if (!isValidURL(git_url)) { 165 | return null; 166 | } 167 | if (!isValidVariable(token)) { 168 | return null; 169 | } 170 | if (!isValidVariable(number)) { 171 | return null; 172 | } 173 | const post_fix_str = postFixToken(token); 174 | return ( 175 | getGitLabAPIURL(git_url) + 176 | 'projects/' + 177 | getGitLabNameSpaceFromGitURL(git_url) + 178 | '%2F' + 179 | getGitLabProjectFromGitURL(git_url) + 180 | '/issues/' + 181 | number + 182 | post_fix_str 183 | ); 184 | }; 185 | 186 | export const getGitLabAPIURLLabel = (git_url, token) => { 187 | if (!isValidURL(git_url)) { 188 | return null; 189 | } 190 | let post_fix_str = postFixToken(token); 191 | post_fix_str += '&per_page=100'; 192 | 193 | return ( 194 | getGitLabAPIURL(git_url) + 195 | 'projects/' + 196 | getGitLabNameSpaceFromGitURL(git_url) + 197 | '%2F' + 198 | getGitLabProjectFromGitURL(git_url) + 199 | '/labels' + 200 | post_fix_str 201 | ); 202 | }; 203 | 204 | export const getGitLabAPIURLMember = (git_url, token) => { 205 | if (!isValidURL(git_url)) { 206 | return null; 207 | } 208 | const post_fix_str = postFixToken(token); 209 | return ( 210 | getGitLabAPIURL(git_url) + 211 | 'projects/' + 212 | getGitLabNameSpaceFromGitURL(git_url) + 213 | '%2F' + 214 | getGitLabProjectFromGitURL(git_url) + 215 | '/members/all' + 216 | post_fix_str + 217 | '&per_page=200' 218 | ); 219 | }; 220 | 221 | export const getGitLabURLIssuebyNumber = (git_url, number) => { 222 | if (!isValidURL(git_url)) { 223 | return null; 224 | } 225 | return ( 226 | getGitLabURL(git_url) + 227 | getGitLabNameSpaceFromGitURLsplitSlash(git_url) + 228 | '/' + 229 | getGitLabProjectFromGitURL(git_url) + 230 | '/-/issues/' + 231 | number 232 | ); 233 | }; 234 | 235 | export const getGitLabURLNewIssueWithTemplate = (git_url) => { 236 | if (!isValidURL(git_url)) { 237 | return null; 238 | } 239 | return ( 240 | getGitLabURL(git_url) + 241 | getGitLabNameSpaceFromGitURLsplitSlash(git_url) + 242 | '/' + 243 | getGitLabProjectFromGitURL(git_url) + 244 | '/issues/new?issue[description]=' 245 | ); 246 | }; 247 | -------------------------------------------------------------------------------- /src/functions/GitLab/GitLabURLHelper.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | getSelfHostingGitLabDomain, 3 | isGitLabURL, 4 | getGitLabDomain, 5 | getGitLabURL, 6 | getGitLabAPIURL, 7 | getGitLabNameSpaceFromGitURL, 8 | getGitLabProjectFromGitURL, 9 | getGitLabAPIURLIssueFilterd, 10 | getGitabAPIURLIssuebyNumber, 11 | getGitLabAPIURLLabel, 12 | getGitLabAPIURLMember, 13 | getGitLabURLIssuebyNumber, 14 | getGitLabURLNewIssueWithTemplate, 15 | } from './GitLabURLHelper'; 16 | 17 | describe('isGitLabURL', () => { 18 | test('true', () => { 19 | expect( 20 | isGitLabURL('https://gitlab.com/lamact/react-issue-ganttchart/') 21 | ).toBe(true); 22 | }); 23 | 24 | test('true', () => { 25 | expect( 26 | isGitLabURL('https://gitlab.com/lamact/react-issue-ganttchart') 27 | ).toBe(true); 28 | }); 29 | 30 | test('true', () => { 31 | expect(isGitLabURL('https://gitlab.com/xxxxxx/yyyyyyyy/zzzzzzzzz')).toBe( 32 | true 33 | ); 34 | }); 35 | 36 | test('false', () => { 37 | expect( 38 | isGitLabURL('https://github.com/lamact/react-issue-ganttchart/') 39 | ).toBe(false); 40 | }); 41 | 42 | test('false', () => { 43 | expect(isGitLabURL('gitlab.com/lamact/react-issue-ganttchart/')).toBe( 44 | false 45 | ); 46 | }); 47 | 48 | test('false', () => { 49 | expect(isGitLabURL('https://gitlab.com/')).toBe(false); 50 | }); 51 | }); 52 | 53 | describe('getSelfHostingGitLabDomain', () => { 54 | test('true', () => { 55 | expect( 56 | getSelfHostingGitLabDomain( 57 | 'https://example.gitlab.com/lamact/react-issue-ganttchart/' 58 | ) 59 | ).toBe('example.gitlab.com'); 60 | }); 61 | test('true', () => { 62 | expect( 63 | getSelfHostingGitLabDomain( 64 | 'https://gitlab.com/lamact/react-issue-ganttchart/' 65 | ) 66 | ).toBe('gitlab.com'); 67 | }); 68 | test('null', () => { 69 | expect( 70 | getSelfHostingGitLabDomain( 71 | 'https://github.com/lamact/react-issue-ganttchart/' 72 | ) 73 | ).toBe(null); 74 | }); 75 | test('null', () => { 76 | expect( 77 | getSelfHostingGitLabDomain('hub.com/lamact/react-issue-ganttchart/') 78 | ).toBe(null); 79 | }); 80 | test('null', () => { 81 | expect(getSelfHostingGitLabDomain('https://gitlab.com')).toBe(null); 82 | }); 83 | }); 84 | 85 | describe('getGitLabDomain', () => { 86 | test('true', () => { 87 | expect( 88 | getGitLabDomain( 89 | 'https://example.gitlab.com/lamact/react-issue-ganttchart/' 90 | ) 91 | ).toBe('https://example.gitlab.com/'); 92 | }); 93 | test('true', () => { 94 | expect( 95 | getGitLabDomain('https://gitlab.com/lamact/react-issue-ganttchart/') 96 | ).toBe('https://gitlab.com/'); 97 | }); 98 | test('null', () => { 99 | expect(getGitLabDomain('htttlab.com/lamact/react-issue-ganttchart/')).toBe( 100 | null 101 | ); 102 | }); 103 | }); 104 | 105 | describe('getGitLabURL', () => { 106 | test('true', () => { 107 | expect( 108 | getGitLabURL('https://gitlab.com/lamact/react-issue-ganttchart/') 109 | ).toBe('https://gitlab.com/'); 110 | }); 111 | test('true', () => { 112 | expect( 113 | getGitLabURL('https://gitlab.com/lamact/react-issue-ganttchart/') 114 | ).toBe('https://gitlab.com/'); 115 | }); 116 | test('null', () => { 117 | expect(getGitLabURL('htttlab.com/lamact/react-issue-ganttchart/')).toBe( 118 | null 119 | ); 120 | }); 121 | }); 122 | 123 | describe('getGitLabAPIURL', () => { 124 | test('true', () => { 125 | expect( 126 | getGitLabAPIURL('https://gitlab.com/lamact/react-issue-ganttchart/') 127 | ).toBe('https://gitlab.com/api/v4/'); 128 | }); 129 | test('true', () => { 130 | expect( 131 | getGitLabAPIURL( 132 | 'https://example.gitlab.com/lamact/react-issue-ganttchart/' 133 | ) 134 | ).toBe('https://example.gitlab.com/api/v4/'); 135 | }); 136 | test('null', () => { 137 | expect(getGitLabAPIURL('htttlab.com/lamact/react-issue-ganttchart/')).toBe( 138 | null 139 | ); 140 | }); 141 | }); 142 | 143 | describe('getGitLabNameSpaceFromGitURL', () => { 144 | test('true', () => { 145 | expect( 146 | getGitLabNameSpaceFromGitURL( 147 | 'https://gitlab.com/lamact/react-issue-ganttchart/' 148 | ) 149 | ).toBe('lamact'); 150 | }); 151 | test('true', () => { 152 | expect( 153 | getGitLabNameSpaceFromGitURL( 154 | 'https://gitlab.com/lamact/react-issue-ganttchart' 155 | ) 156 | ).toBe('lamact'); 157 | }); 158 | test('true', () => { 159 | expect( 160 | getGitLabNameSpaceFromGitURL( 161 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart' 162 | ) 163 | ).toBe('lamact%2Fsubgrp'); 164 | }); 165 | test('true', () => { 166 | expect( 167 | getGitLabNameSpaceFromGitURL( 168 | 'https://ex.gitlab.com/lamact/react-issue-ganttchart/' 169 | ) 170 | ).toBe('lamact'); 171 | }); 172 | test('null', () => { 173 | expect( 174 | getGitLabNameSpaceFromGitURL( 175 | 'htts://gitab.com/lamact/react-issue-ganttchart/' 176 | ) 177 | ).toBe(null); 178 | }); 179 | test('null', () => { 180 | expect(getGitLabNameSpaceFromGitURL('https://ex.gitlab.com')).toBe(null); 181 | }); 182 | }); 183 | 184 | describe('getGitLabProjectFromGitURL', () => { 185 | test('true', () => { 186 | expect( 187 | getGitLabProjectFromGitURL( 188 | 'https://gitlab.com/lamact/react-issue-ganttchart/' 189 | ) 190 | ).toBe('react-issue-ganttchart'); 191 | }); 192 | test('true', () => { 193 | expect( 194 | getGitLabProjectFromGitURL( 195 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/' 196 | ) 197 | ).toBe('react-issue-ganttchart'); 198 | }); 199 | test('true', () => { 200 | expect( 201 | getGitLabProjectFromGitURL( 202 | 'https://ex.gitlab.com/lamact/react-issue-ganttchart/' 203 | ) 204 | ).toBe('react-issue-ganttchart'); 205 | }); 206 | test('null', () => { 207 | expect( 208 | getGitLabProjectFromGitURL('httitab.com/lamact/react-issue-ganttchart/') 209 | ).toBe(null); 210 | }); 211 | test('null', () => { 212 | expect(getGitLabProjectFromGitURL('https://ex.gitlab.com')).toBe(null); 213 | }); 214 | }); 215 | 216 | describe('getGitLabAPIURLIssueFilterd', () => { 217 | test('true', () => { 218 | expect( 219 | getGitLabAPIURLIssueFilterd( 220 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 221 | 'privateaccesstoken', 222 | [ 223 | { id: 123, name: 'Todo' }, 224 | { id: 124, name: 'Doing' }, 225 | ], 226 | { id: 12, name: 'satoshi' } 227 | ) 228 | ).toBe( 229 | 'https://gitlab.com/api/v4/projects/lamact%2Freact-issue-ganttchart/issues?access_token=privateaccesstoken&labels=Todo,Doing,&assignee_id=12&per_page=100&state=opened' 230 | ); 231 | }); 232 | test('true', () => { 233 | expect( 234 | getGitLabAPIURLIssueFilterd( 235 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/', 236 | 'privateaccesstoken', 237 | [ 238 | { id: 123, name: 'Todo' }, 239 | { id: 124, name: 'Doing' }, 240 | ], 241 | { id: 12, name: 'satoshi' } 242 | ) 243 | ).toBe( 244 | 'https://gitlab.com/api/v4/projects/lamact%2Fsubgrp%2Freact-issue-ganttchart/issues?access_token=privateaccesstoken&labels=Todo,Doing,&assignee_id=12&per_page=100&state=opened' 245 | ); 246 | }); 247 | test('null', () => { 248 | expect( 249 | getGitLabAPIURLIssueFilterd( 250 | 'notvalidurl', 251 | 'privateaccesstoken', 252 | [ 253 | { id: 123, name: 'Todo' }, 254 | { id: 124, name: 'Doing' }, 255 | ], 256 | { id: 12, name: 'satoshi' } 257 | ) 258 | ).toBe(null); 259 | }); 260 | test('null', () => { 261 | expect( 262 | getGitLabAPIURLIssueFilterd( 263 | null, 264 | 'privateaccesstoken', 265 | [ 266 | { id: 123, name: 'Todo' }, 267 | { id: 124, name: 'Doing' }, 268 | ], 269 | { id: 12, name: 'satoshi' } 270 | ) 271 | ).toBe(null); 272 | }); 273 | test('null', () => { 274 | expect( 275 | getGitLabAPIURLIssueFilterd( 276 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 277 | null, 278 | [ 279 | { id: 123, name: 'Todo' }, 280 | { id: 124, name: 'Doing' }, 281 | ], 282 | { id: 12, name: 'satoshi' } 283 | ) 284 | ).toBe(null); 285 | }); 286 | test('null', () => { 287 | expect( 288 | getGitLabAPIURLIssueFilterd( 289 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 290 | 'privateaccesstoken', 291 | null, 292 | { id: 12, name: 'satoshi' } 293 | ) 294 | ).toBe(null); 295 | }); 296 | test('null', () => { 297 | expect( 298 | getGitLabAPIURLIssueFilterd( 299 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 300 | 'privateaccesstoken', 301 | [ 302 | { id: 123, name: 'ToDo' }, 303 | { id: 124, name: 'Doing' }, 304 | ], 305 | null 306 | ) 307 | ).toBe(null); 308 | }); 309 | test('null', () => { 310 | expect( 311 | getGitLabAPIURLIssueFilterd( 312 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 313 | 'privateaccesstoken', 314 | [ 315 | { id: 123, name: 'ToDo' }, 316 | { id: 124, name: 'Doing' }, 317 | ], 318 | { name: 'satoshi' } 319 | ) 320 | ).toBe(null); 321 | }); 322 | }); 323 | 324 | describe('getGitabAPIURLIssuebyNumber', () => { 325 | test('true', () => { 326 | expect( 327 | getGitabAPIURLIssuebyNumber( 328 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 329 | 'privateaccesstoken', 330 | 325 331 | ) 332 | ).toBe( 333 | 'https://gitlab.com/api/v4/projects/lamact%2Freact-issue-ganttchart/issues/325?access_token=privateaccesstoken' 334 | ); 335 | }); 336 | test('true', () => { 337 | expect( 338 | getGitabAPIURLIssuebyNumber( 339 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/', 340 | 'privateaccesstoken', 341 | 325 342 | ) 343 | ).toBe( 344 | 'https://gitlab.com/api/v4/projects/lamact%2Fsubgrp%2Freact-issue-ganttchart/issues/325?access_token=privateaccesstoken' 345 | ); 346 | }); 347 | test('null', () => { 348 | expect( 349 | getGitabAPIURLIssuebyNumber( 350 | 'httpm/lamact/react-issue-ganttchart/', 351 | 'privateaccesstoken', 352 | 325 353 | ) 354 | ).toBe(null); 355 | }); 356 | test('null', () => { 357 | expect( 358 | getGitabAPIURLIssuebyNumber( 359 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 360 | null, 361 | 325 362 | ) 363 | ).toBe(null); 364 | }); 365 | test('null', () => { 366 | expect( 367 | getGitabAPIURLIssuebyNumber( 368 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 369 | 'privateaccesstoken', 370 | null 371 | ) 372 | ).toBe(null); 373 | }); 374 | }); 375 | 376 | describe('getGitLabAPIURLLabel', () => { 377 | test('true', () => { 378 | expect( 379 | getGitLabAPIURLLabel( 380 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 381 | 'privateaccesstoken' 382 | ) 383 | ).toBe( 384 | 'https://gitlab.com/api/v4/projects/lamact%2Freact-issue-ganttchart/labels?access_token=privateaccesstoken&per_page=100' 385 | ); 386 | }); 387 | test('true', () => { 388 | expect( 389 | getGitLabAPIURLLabel( 390 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/', 391 | 'privateaccesstoken' 392 | ) 393 | ).toBe( 394 | 'https://gitlab.com/api/v4/projects/lamact%2Fsubgrp%2Freact-issue-ganttchart/labels?access_token=privateaccesstoken&per_page=100' 395 | ); 396 | }); 397 | test('null', () => { 398 | expect( 399 | getGitLabAPIURLLabel( 400 | 'httom/lamact/react-issue-ganttchart/', 401 | 'privateaccesstoken' 402 | ) 403 | ).toBe(null); 404 | }); 405 | }); 406 | 407 | describe('getGitLabAPIURLMember', () => { 408 | test('true', () => { 409 | expect( 410 | getGitLabAPIURLMember( 411 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 412 | 'privateaccesstoken' 413 | ) 414 | ).toBe( 415 | 'https://gitlab.com/api/v4/projects/lamact%2Freact-issue-ganttchart/members/all?access_token=privateaccesstoken&per_page=200' 416 | ); 417 | }); 418 | test('true', () => { 419 | expect( 420 | getGitLabAPIURLMember( 421 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/', 422 | 'privateaccesstoken' 423 | ) 424 | ).toBe( 425 | 'https://gitlab.com/api/v4/projects/lamact%2Fsubgrp%2Freact-issue-ganttchart/members/all?access_token=privateaccesstoken&per_page=200' 426 | ); 427 | }); 428 | test('null', () => { 429 | expect( 430 | getGitLabAPIURLMember( 431 | 'httom/lamact/react-issue-ganttchart/', 432 | 'privateaccesstoken' 433 | ) 434 | ).toBe(null); 435 | }); 436 | }); 437 | 438 | describe('getGitLabURLIssuebyNumber', () => { 439 | test('true', () => { 440 | expect( 441 | getGitLabURLIssuebyNumber( 442 | 'https://gitlab.com/lamact/react-issue-ganttchart/', 443 | 43 444 | ) 445 | ).toBe( 446 | 'https://gitlab.com/lamact/react-issue-ganttchart/-/issues/43' 447 | ); 448 | }); 449 | test('true', () => { 450 | expect( 451 | getGitLabURLIssuebyNumber( 452 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/', 453 | 43 454 | ) 455 | ).toBe( 456 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/-/issues/43' 457 | ); 458 | }); 459 | test('null', () => { 460 | expect( 461 | getGitLabURLIssuebyNumber('httom/lamact/react-issue-ganttchart/', 43) 462 | ).toBe(null); 463 | }); 464 | }); 465 | 466 | 467 | describe('getGitLabURLNewIssueWithTemplate', () => { 468 | test('true', () => { 469 | expect( 470 | getGitLabURLNewIssueWithTemplate( 471 | 'https://gitlab.com/lamact/react-issue-ganttchart/' 472 | ) 473 | ).toBe( 474 | 'https://gitlab.com/lamact/react-issue-ganttchart/issues/new?issue[description]=' 475 | ); 476 | }); 477 | test('true', () => { 478 | expect( 479 | getGitLabURLNewIssueWithTemplate( 480 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/' 481 | ) 482 | ).toBe( 483 | 'https://gitlab.com/lamact/subgrp/react-issue-ganttchart/issues/new?issue[description]=' 484 | ); 485 | }); 486 | test('null', () => { 487 | expect( 488 | getGitLabURLNewIssueWithTemplate('httom/lamact/react-issue-ganttchart/') 489 | ).toBe(null); 490 | }); 491 | }); 492 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | min-width: 1200px; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router, Route } from "react-router-dom"; 4 | import './index.css'; 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render( 9 | 10 | } /> 11 | , 12 | document.getElementById("root")); 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker === null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | --------------------------------------------------------------------------------