Compare commits

..
243 Commits
Author SHA1 Message Date
Maksim Eltyshev 1a6fc40804 chore: Update version 2022-08-26 19:13:10 +02:00
Maksim Eltyshev 42092efcd2 fix: Remove dots in translations 2022-08-26 19:11:32 +02:00
Maksim Eltyshev 5c91bddfe7 feat: Stronger password policy 2022-08-26 18:59:44 +02:00
Maksim Eltyshev fd8f40f1b0 chore: Update version 2022-08-26 16:05:47 +02:00
Maksim Eltyshev 5b64d465e2 fix: Use move-file instead of rename 2022-08-26 16:02:47 +02:00
Maksim Eltyshev 212fb8ac8c fix: Add missing border radius 2022-08-26 02:47:43 +02:00
Maksim Eltyshev cece2254d7 fix: Change mechanics of file uploading 2022-08-26 02:45:27 +02:00
Maksim Eltyshev c940805f14 chore: Fix version 2022-08-24 15:44:01 +02:00
Maksim Eltyshev 052ab7f653 chore: Update version 2022-08-24 15:37:27 +02:00
Maksim Eltyshev 5a38805588 feat: Add Italian translation. Thanks to @belf88 2022-08-24 15:27:12 +02:00
Maksim Eltyshev 2dbbead2e9 fix: Add missing border radiuses 2022-08-23 21:19:43 +02:00
Maksim Eltyshev 039eee0aac ref: Little refactoring 2022-08-23 21:06:50 +02:00
Maksim Eltyshev 07867fc0b2 fix: Use custom logger only for production 2022-08-23 20:45:42 +02:00
Maksim Eltyshev 55d07b4560 meta: Fixes in readme 2022-08-23 01:14:11 +02:00
Maksim Eltyshev 289d2bf5ae meta: Fix ignore files for logs 2022-08-23 00:51:33 +02:00
Steven Correia 6429e22d59 feat: Modify logger to log to file that supports fail2ban (#284) 2022-08-23 03:42:56 +05:00
Maksim Eltyshev 21ef850913 fix: Fix missed function name 2022-08-22 16:27:14 +02:00
Maksim Eltyshev e4769220da chore: Update version 2022-08-19 14:04:26 +02:00
Maksim Eltyshev 378e02c0a4 Merge branch 'master' of https://github.com/plankanban/planka 2022-08-19 14:01:24 +02:00
Maksim Eltyshev 51fa7df69c feat: Permissions for board members
Closes #262
2022-08-19 14:00:40 +02:00
Maksim Eltyshev 68806b9080 meta: Add sponsor button 2022-08-18 16:51:59 +05:00
Maksim Eltyshev d80a538857 fix: Little improvements in users modal 2022-08-17 02:00:24 +02:00
Maksim Eltyshev 78669da01d feat: Add search by users, members and labels 2022-08-17 00:53:08 +02:00
Maksim Eltyshev 3a7720974b chore: Update version 2022-08-09 22:34:16 +02:00
Maksim Eltyshev eea94e0ee2 ref: Little change for consistency 2022-08-09 22:31:43 +02:00
Maksim Eltyshev 4555dafd75 meta: Add vscode config 2022-08-09 19:36:08 +02:00
Maksim Eltyshev 95ebb27042 ref: Prettify env sample 2022-08-09 19:17:19 +02:00
Maksim Eltyshev d38e555293 fix: Provide default value for token expiration 2022-08-09 19:09:07 +02:00
SimonTagne 7786533a90 feat: Improve security of access tokens (#279)
Closes #275
2022-08-09 21:03:21 +05:00
SimonTagne dab38cbc18 ref: Ignore public js linting (#278) 2022-08-08 17:58:41 +05:00
Maksim Eltyshev 3714bbc06f ref: Refactoring 2022-08-04 13:31:14 +02:00
Maksim Eltyshev 1f569bdb61 chore: Update version 2022-08-04 00:41:57 +02:00
Maksim Eltyshev ac1df5201d fix: Fix path traversal vulnerability 2022-08-04 00:37:30 +02:00
Maksim Eltyshev fbe24c0fc0 chore: Update version 2022-08-03 13:28:41 +02:00
Maksim Eltyshev 79daed6afa docs: Add security policy
Closes #276
2022-08-03 16:15:40 +05:00
Maksim Eltyshev a9111bf7ba feat: Add Korean translation. Thanks to @chooya 2022-08-03 12:21:01 +02:00
Maksim Eltyshev 2d50211892 fix: Fix label style 2022-08-01 00:18:22 +02:00
Maksim Eltyshev 139971eca9 chore: Update version 2022-07-29 17:50:11 +02:00
Maksim Eltyshev 34db8947b6 feat: Toggle actions details, little redesign 2022-07-29 17:40:18 +02:00
Maksim Eltyshev 82e4f73c1f ref: Fix variable names 2022-07-29 17:11:38 +02:00
Maksim Eltyshev 560bddbe18 fix: Fix styles 2022-07-26 13:53:30 +02:00
Maksim Eltyshev 9c192f4b5c fix: Fix select behaviour 2022-07-26 12:49:40 +02:00
Maksim Eltyshev a148a3aae5 feat: Add language selector
Closes #212
2022-07-26 12:26:42 +02:00
Maksim Eltyshev 7c1b885d2a chore: Update version 2022-07-24 17:17:59 +02:00
Maksim Eltyshev a3f33d0361 fix: Fix core locale loading 2022-07-24 17:13:28 +02:00
Maksim Eltyshev fe62180cda chore: Update dependencies 2022-07-24 17:04:26 +02:00
Maksim Eltyshev 58abd0691f chore: Update version 2022-07-21 11:42:56 +02:00
Maksim Eltyshev ad67b9ba24 feat: Add tasks reordering
Closes #50, closes #232
2022-07-21 11:31:05 +02:00
Maksim Eltyshev ddf9a32ea9 chore: Update version 2022-06-24 17:20:44 +02:00
Maksim Eltyshev e01a2eafba fix: Fix crash when uploading attachment 2022-06-24 17:17:42 +02:00
Maksim Eltyshev 3b3bff1b6b fix: Allow to use endpoints via http 2022-06-24 11:59:04 +02:00
Maksim Eltyshev ecc45c7a9e chore: Update version 2022-06-22 18:19:34 +02:00
Maksim Eltyshev 3497e09703 feat: Add pdf viewer to attachments gallery 2022-06-22 17:01:35 +02:00
Maksim Eltyshev dfc52191fc chore: Update version 2022-06-22 09:09:44 +02:00
Maksim Eltyshev 6268ced4cb ref: Collapse image data into one column 2022-06-21 12:01:41 +02:00
Maksim Eltyshev e7495e0067 fix: Fix routes again 2022-06-20 18:48:13 +02:00
Maksim Eltyshev b8c065e0c8 fix: Fix routes 2022-06-20 18:46:03 +02:00
Maksim Eltyshev 86e4864d1b feat: Add gallery for attachments 2022-06-20 18:27:39 +02:00
Maksim Eltyshev 0be598ca9e chore: Update version 2022-06-17 18:17:08 +02:00
Dominik Dancs ba850801c5 feat: Add Slovak translation (#265) 2022-06-17 20:16:15 +05:00
Maksim Eltyshev c9b23652d1 feat: Add ability to edit users
Closes #60
2022-06-15 14:13:22 +02:00
Maksim Eltyshev 8490629e4c fix: Fix password edit popup styles 2022-06-13 17:33:26 +02:00
ejo090 76fcd79203 fix: Fix markdown line breaks
Closes #257
2022-06-06 17:37:43 +05:00
Maksim Eltyshev 3ce1bc0419 fix: Fix dependencies installation 2022-05-21 23:52:12 +05:00
Maksim Eltyshev 540fbfceb5 chore: Update version 2022-05-21 21:49:36 +05:00
Maksim Eltyshev 48a56648e2 chore: Update version 2022-05-21 21:30:38 +05:00
Maksim Eltyshev 6d002c311c fix: Fix Swedish language code 2022-05-21 21:29:33 +05:00
Maksim Eltyshev ede707ed24 fix: Fix dependencies installation for multi-arch images 2022-05-21 21:01:43 +05:00
André Wahlberg 81052e1ce6 feat: Add Swedish translation (#246) 2022-05-21 20:39:17 +05:00
Maksim Eltyshev 315a9a4c3d ci: Fix build-args input name 2022-05-05 02:46:30 +05:00
Maksim Eltyshev 8112bcbb84 ci: Fix build_args 2022-05-05 00:34:26 +05:00
Maksim Eltyshev fc69b7d6f4 ci: Add manual trigger to build-and-push-docker-base-image action 2022-05-05 00:18:00 +05:00
Aliaksandr Shulyak 027e911907 Improvements for docker build process (#237) 2022-05-05 00:04:19 +05:00
Maksim Eltyshev afc2a4628d chore: Update version 2022-04-30 20:34:56 +05:00
Maksim Eltyshev b0b6587eee fix: Fix attachment headers
Closes #231
2022-04-29 17:21:39 +05:00
Aliaksandr Shulyak e29c9e6dda ci: GitHub actions config for docker builds (#221) 2022-04-29 17:15:59 +05:00
Maksim Eltyshev caf9602557 chore: Update version 2022-04-28 14:23:11 +05:00
Maksim Eltyshev 445b0fc682 fix: Change way to fix application crash when deleting project 2022-04-28 14:15:36 +05:00
Maksim Eltyshev 2e045217ec fix: Fix application crash after deleting project 2022-04-28 00:17:15 +05:00
Maksim Eltyshev 36e4bef21b feat: Remove attachments from public access
Closes #219
2022-04-26 22:20:20 +05:00
Maksim Eltyshev 486e663a3d feat: Store accessToken in cookies instead of localStorage 2022-04-26 18:01:55 +05:00
Michael Fürnschuß 536064fdfe fix: Fix missed table name (#227) 2022-04-25 16:00:14 +05:00
Michael Fürnschuß f03bb5a78f feat: Update German translation (#228) 2022-04-25 15:45:24 +05:00
Maksim Eltyshev 8bf85e2eb6 ref: Rename .env file to sample, stop tracking .env file 2022-04-22 16:21:09 +05:00
Maksim Eltyshev e3d1d36857 fix: Fix markdown options and styles 2022-04-22 02:06:12 +05:00
Maksim Eltyshev 4371e16c8d feat: Fallback to English if translation is missing
Closes #175
2022-04-21 04:42:02 +05:00
Maksim Eltyshev ec7b4d7b24 ref: Fix locale file names 2022-04-21 04:20:04 +05:00
Maksim Eltyshev 7974c9dc5d docs: Add contributing guidelines 2022-04-20 19:36:24 +05:00
Michal VOLF e2a8f5b483 feat: Update Czech translation (#217) 2022-04-20 14:17:00 +05:00
Arcady "RKD" Chumachenko 5049034ae3 feat: Alway apply migrations on start-up (#172) 2022-04-20 04:52:42 +05:00
Maksim Eltyshev 16a822cc7d Prepare for release 2022-04-19 20:36:10 +05:00
Maksim Eltyshev 6f219e5368 Fix pre-commit hook, update dependencies 2022-04-19 02:52:19 +05:00
Maksim Eltyshev e239d85901 Update dependencies 2022-04-16 00:08:14 +05:00
Maksim Eltyshev f539e7b76d Fix language loading. Closes #207 2022-03-31 02:23:44 +05:00
Maksim Eltyshev 0c92623109 Update dependencies 2022-02-09 00:56:01 +05:00
Lorenz Brun 95ea1e97d3 Improve Docker cache efficiency and fix signal handling 2022-02-08 16:15:52 +05:00
Maksim Eltyshev 6f99d99683 Add date-fns locale for Danish language 2022-01-30 02:50:13 +05:00
Christiansen afdcd11da6 Add Danish translation 2022-01-30 02:46:41 +05:00
Maksim Eltyshev a628f116a7 Fix linting errors, fix default database url. Closes #193 2021-12-30 00:30:53 +05:00
Asliddinbek Azizovich 6b158f0f1d Add Uzbek language 2021-12-15 16:16:20 +05:00
RokkuCode f59981db79 Fix several spelling errors and grammar in German translation 2021-12-15 16:11:10 +05:00
MoaufmKlo 96fa39e830 Correct incorrect gender in German translation 2021-12-15 16:04:36 +05:00
MoaufmKlo 2efc20cc02 Improve German translation 2021-10-21 01:39:21 +05:00
SimonTagne 31d314736a Fix updates of username and email 2021-10-21 01:25:04 +05:00
dbrennand 729e5dac5e Make trustProxy configurable via environment variable 2021-10-21 01:24:19 +05:00
ejo090 07e58ffe0c Add Japanese language 2021-10-21 01:22:28 +05:00
Maksim Eltyshev 32aae89c6e Merge branch 'master' of https://github.com/plankanban/planka 2021-08-02 17:24:54 +05:00
Maksim Eltyshev 1a5c0f53f8 Fix board deletion, update dependencies. Closes #146 2021-08-02 17:23:56 +05:00
SohnyBohny b63f0742ba Fix invalid white-spaces in card description. Closes #141 2021-07-12 19:16:54 +05:00
Maksim Eltyshev b39119ace4 Project managers, board members, auto-update after reconnection, refactoring 2021-06-24 01:05:22 +05:00
William Puckering d6cb1f6683 Fix misspelling of «management». Closes #120 2021-04-30 20:26:19 +05:00
Maksim Eltyshev 0f0ae5f483 Add date-fns locale for Spanish language 2021-04-28 00:01:35 +05:00
Santiago Forero f45b5c704f Add Spanish language 2021-04-27 23:53:18 +05:00
Alfred Feldmeyer f123254ea3 Ignore custom docker-compose overrides 2021-04-13 19:08:30 +05:00
Jan Metzger f6dbba2ba9 Fix project title word break on projects overview page 2021-04-13 19:03:23 +05:00
Maksim Eltyshev 0fb60eb886 Allow dots in username. Closes #116 2021-04-13 18:59:02 +05:00
Maksim Eltyshev fb42ed460e Set best image quality for all uploads. Closes #114 2021-04-13 18:47:56 +05:00
Michal VOLF 141fc74dbb Fix datepicker language code for Czech language 2021-03-27 17:50:02 +05:00
Maksim Eltyshev 65aba551c3 Add Czech language. Thanks to @michal-volf 2021-03-26 11:19:43 +05:00
Maksim Eltyshev a3d1a8a09a Add support for GitHub Flavored Markdown. Closes #107 2021-03-25 21:32:31 +05:00
Maksim Eltyshev cf21bef9ee Fix file attach from clipboard when copy from browser. Closes #96 2021-03-20 03:33:16 +05:00
nielsboussion def1134566 Add French language 2021-03-20 02:30:45 +05:00
Nick Kelly a3855f8080 Enable Redux DevTools in development mode 2021-02-16 03:14:43 +05:00
Maksim Eltyshev f718464940 Fix variables in update-card, update dependencies. Closes #85 2021-02-10 17:39:41 +05:00
pkrolkgp 83ad2f45e6 Fix typos in polish translation 2021-01-11 11:10:22 +05:00
Maksim Eltyshev 3d6f9cc0e5 Adjust label width, update dependencies. Closes #75 2021-01-09 18:42:16 +05:00
Maksim Eltyshev 2b318fdc7a Add date-fns locale for polish language 2020-12-20 18:45:33 +05:00
pkrolkgp 73c3bf3728 Add polish language 2020-12-20 18:37:55 +05:00
Maksim Eltyshev 179ad3610f Update dependencies 2020-12-18 01:02:28 +05:00
Stefan Mielke 0eb14c34ef Fix german translations 2020-12-13 23:31:10 +05:00
Maksim Eltyshev 3cdb614b57 Fix loading deleted users on actions fetch. Closes #62 2020-12-13 23:17:12 +05:00
Maksim Eltyshev a3adddac6f Add german language. Closes #54 2020-11-30 21:05:40 +05:00
timohzbs 9f6a852da3 Add core.js de locale 2020-11-30 20:43:53 +05:00
timohzbs 0cf0e84ded Add embed.js de locale 2020-11-30 20:43:25 +05:00
Maksim Eltyshev de1f7c7564 Update dependencies 2020-11-20 06:01:42 +05:00
Maksim Eltyshev de7d080910 Add ability to create multi-arch images. Closes #39, closes #47 2020-11-17 17:01:18 +05:00
Maksim Eltyshev 771c443694 Add z-index to popup container. Closes #46 2020-10-21 14:50:15 +05:00
Maksim Eltyshev c28fada7ee Update readme, update dependencies 2020-10-20 16:10:25 +05:00
Maksim Eltyshev 437ac523bc Update readme 2020-10-17 23:49:19 +05:00
Maksim Eltyshev 6584d1cb5d Create FUNDING.yml 2020-10-06 21:12:14 +05:00
Maksim Eltyshev e47697b0c5 Update readme 2020-10-06 21:07:14 +05:00
Maksim Eltyshev cc1358b477 Update readme 2020-10-06 21:04:43 +05:00
Maksim Eltyshev ab020fc324 Update dependencies 2020-09-28 04:40:10 +05:00
Maksim Eltyshev 08aef7c052 Change database adapter to support the new Node. Closes #32 2020-08-31 16:45:22 +05:00
Maksim Eltyshev 767d39586c Add test libs, update dependencies 2020-08-20 15:35:46 +05:00
Maksim Eltyshev 089668ed10 Fix modal type variable. Closes #20 2020-08-13 11:38:47 +05:00
Maksim Eltyshev 8fc6bb2e3e Update dependencies 2020-08-11 20:45:35 +05:00
Maksim Eltyshev c6ee7d54bb Prepare for collection board type, refactoring, update dependencies 2020-08-04 01:32:46 +05:00
Maksim Eltyshev 2d92ade8dc Add scrollbar for board tabs, update dependencies 2020-06-08 18:49:40 +05:00
Maksim Eltyshev c4e3c3d4c8 Return text-shadow back, make project wrapper less transparent 2020-06-04 21:16:41 +05:00
Maksim Eltyshev d22580cec0 Fix removing background 2020-06-04 17:52:30 +05:00
Maksim Eltyshev 99d9ab4a9e Add validation to background field 2020-06-03 22:27:20 +05:00
Maksim Eltyshev ab83d7c9ab Fix repository language 2020-05-29 21:46:08 +05:00
Maksim Eltyshev ff95a12578 Background gradients, migrate from CSS to SCSS, remove !important 2020-05-29 19:31:19 +05:00
Maksim Eltyshev 196121bd38 Update readme 2020-05-27 15:20:00 +05:00
Maksim Eltyshev 313f7ab7c7 Remove text-shadow everywhere 2020-05-27 12:50:37 +05:00
Maksim Eltyshev 38f68c1b82 Update dependencies 2020-05-26 14:10:06 +05:00
Maksim Eltyshev 99807c2a49 Fix Docker volumes 2020-05-26 01:32:11 +05:00
Maksim Eltyshev 2f7a244807 Add project backgrounds 2020-05-26 00:46:04 +05:00
Maksim Eltyshev fb9ceb5db7 Fix border in card modal 2020-05-23 18:05:22 +05:00
Maksim Eltyshev efd4de4000 Transparent color scheme, preparing for custom backgrounds 2020-05-22 16:59:22 +05:00
Maksim Eltyshev 746e2fe790 Make columns itself scrollable, fix action creation when moving card, little refactoring 2020-05-16 04:09:46 +05:00
Maksim Eltyshev c5b44598f9 Fix members and labels creation when transfer card to another board 2020-05-10 02:06:14 +05:00
Maksim Eltyshev 35f9631326 Preserve members and labels when transfer card to another board 2020-05-09 05:30:52 +05:00
Maksim Eltyshev ca6efd5b71 Set default Postgres user, delete user creation information from readme 2020-05-06 22:37:45 +05:00
Maksim Eltyshev 6a7ad7a24b Update dependencies 2020-05-06 21:21:18 +05:00
Maksim Eltyshev 0519df4e4d Merge pull request #12 from wheresvic/master
Add dev documentation and local db server
2020-05-06 19:07:23 +05:00
Victor Parmar deec1a06a2 c add dev documentation and local db server 2020-05-06 00:07:23 +02:00
Maksim Eltyshev f4c8bc6ab9 Remove members and labels when transfer card to another board 2020-05-05 03:33:29 +05:00
Maksim Eltyshev cbc942e306 Move cards between boards and projects 2020-05-05 01:30:06 +05:00
Maksim Eltyshev 712567ac57 Change user size on card, save card description on click outside, prevent list deletion if any filter is active 2020-04-30 05:27:46 +05:00
Maksim Eltyshev a9f2948672 Add attachment popup, hide attachments if more than 4 2020-04-29 23:55:16 +05:00
Maksim Eltyshev 844b3ab8f1 Fix Docker image, update dependencies 2020-04-29 04:50:32 +05:00
Maksim Eltyshev 246eb17249 Add dropzone for attachment, paste attachment from clipboard 2020-04-28 19:46:55 +05:00
Maksim Eltyshev e1364925ed Link all scripts to the root package.json, start development servers concurrently 2020-04-25 04:57:31 +05:00
Maksim Eltyshev 5481ccb91f Add start script for Docker image, pre-commit lint check, package.json scripts cleanup 2020-04-24 21:44:54 +05:00
Maksim Eltyshev 73bd7aab89 Fix path to Docker Hub 2020-04-23 23:03:40 +05:00
Maksim Eltyshev 63807c7db6 Update repository paths because of transfer 2020-04-23 22:56:25 +05:00
Maksim Eltyshev ce39b584ca Fix attachment duplication on add 2020-04-23 05:56:02 +05:00
Maksim Eltyshev 3e85371da9 Fix attachment thumbnail label color 2020-04-23 03:49:39 +05:00
Maksim Eltyshev 3dffed90c6 Add covers for cards 2020-04-23 03:02:53 +05:00
Maksim Eltyshev f743f4ea8b Add file attachments 2020-04-21 05:44:12 +05:00
Maksim Eltyshev 87a3cf751a Fix language detection for some browsers 2020-04-11 01:02:08 +05:00
Maksim Eltyshev be8a806102 Update dependencies 2020-04-10 00:24:05 +05:00
Maksim Eltyshev 88314e826d Add preferences tab to user settings, add subscribe to own cards option 2020-04-10 00:11:34 +05:00
Maksim Eltyshev 9b4e3931a9 Add phone and organization fields to user 2020-04-09 18:27:28 +05:00
Maksim Eltyshev c6ecf126d0 Add user settings modal 2020-04-08 21:12:58 +05:00
Maksim Eltyshev af00e3e191 Add username to user 2020-04-03 00:35:25 +05:00
Maksim Eltyshev ec222a5e32 Update readme 2020-03-31 14:03:50 +05:00
Maksim Eltyshev bfd21ec05a Add new label colors 2020-03-31 03:00:58 +05:00
Maksim Eltyshev d1b74f9d11 Update dependencies 2020-03-25 00:15:47 +05:00
Maksim Eltyshev 4da29f2d8e Update dependencies 2020-03-14 20:58:48 +05:00
Maksim Eltyshev de7a8cd8d4 Downgrade bcrypt for Alpine Linux compatibility 2020-02-25 18:42:00 +05:00
Maksim Eltyshev b93c5aa070 Update dependencies 2020-02-25 02:10:27 +05:00
Maksim Eltyshev 5083e7ad57 Rename user table, remove ES6 import. Closes #6, closes #7 2020-02-25 01:33:54 +05:00
Maksim Eltyshev 9c7c96a780 Move from prettier-eslint to eslint-plugin-prettier, update dependencies 2020-02-03 18:42:31 +05:00
Maksim Eltyshev f0e7fb8fd1 Update dependencies 2020-01-14 13:10:34 +05:00
Maksim Eltyshev dc0f71624b Update dependencies 2019-12-30 04:24:06 +05:00
Maksim Eltyshev 622f56c77c Update Alpine repository branch in Dockerfile. Closes #4 2019-12-12 13:05:55 +05:00
Maksim Eltyshev 8b1ca9e52b Update dependencies 2019-12-11 04:23:33 +05:00
Maksim Eltyshev b4fd32fd17 Update dependencies 2019-11-18 11:59:37 +05:00
Maksim Eltyshev 00c7f514ae Add visibility toggle of password input. Closes #2 2019-11-15 03:45:59 +05:00
Maksim Eltyshev a11f6260c0 Code formatting with prettier, change eslint config for the server 2019-11-05 18:01:42 +05:00
Maksim Eltyshev b7f37f0f96 Fix handler in form/field hooks 2019-10-31 23:07:41 +05:00
Maksim Eltyshev 27939cbd5e Update dependencies 2019-10-30 21:21:01 +05:00
Maksim Eltyshev 4a5980cd82 Update dependencies 2019-10-29 14:19:51 +05:00
Maksim Eltyshev c6bbd038e2 Update dependencies 2019-10-28 23:23:36 +05:00
Maksim Eltyshev 3896e0688e Update dependencies 2019-10-26 03:51:36 +05:00
Maksim Eltyshev 859c09d05e Fix api transformers, intercept by error message instead of undefined 2019-10-25 21:38:14 +05:00
Maksim Eltyshev 17d2781d33 Update dependencies 2019-10-24 23:45:22 +05:00
Maksim Eltyshev f96c3e83de Update dependencies 2019-10-23 23:01:06 +05:00
Maksim Eltyshev 671bfc5efa Update dependencies 2019-10-22 19:46:11 +05:00
Maksim Eltyshev 0e9ae2a715 Remove axios from dependencies 2019-10-21 16:57:15 +05:00
Maksim Eltyshev 97856411a9 Update dependencies 2019-10-20 23:35:09 +05:00
Maksim Eltyshev e07b0bc5b1 Fix timer counting, fix flickering on comment creation, update dependencies 2019-10-19 17:11:37 +05:00
Maksim Eltyshev 2566ff376e Add email and password change functionality for a current user, remove deep compare hooks 2019-10-18 08:06:34 +05:00
Maksim Eltyshev e564729598 Fix typo in en-US translation. Closes #1 2019-10-17 18:28:15 +05:00
Maksim Eltyshev 1f38c6073c Small UI improvements, update dependencies 2019-10-16 22:48:24 +05:00
Maksim Eltyshev d770470371 Update gitignore 2019-10-15 20:36:18 +05:00
Maksim Eltyshev 5b9cad42fd Update server scaffold 2019-10-14 21:50:52 +05:00
Maksim Eltyshev dea090051c Update dependencies 2019-10-13 07:50:45 +05:00
Maksim Eltyshev 1d047c8e55 Update favicon and manifest files 2019-10-11 19:50:31 +05:00
Maksim Eltyshev dab8159864 Update readme 2019-10-11 16:28:23 +05:00
Maksim Eltyshev e8139b29d5 Change id generation algorithm, display notifications total on the main page 2019-10-10 14:51:59 +05:00
Maksim Eltyshev 4911816734 Allow creating label without name 2019-10-09 18:48:19 +05:00
Maksim Eltyshev b9d4024957 Fix main page scrollbar and socket status position 2019-10-09 01:05:06 +05:00
Maksim Eltyshev dc09246d8a Update dependencies 2019-10-07 21:59:29 +05:00
Maksim Eltyshev 3b050fe5b8 Update readme 2019-10-05 20:46:34 +05:00
Maksim Eltyshev a09a53ed12 Add code of conduct 2019-10-05 19:44:16 +05:00
Maksim Eltyshev 51013b6a32 Rename deadline to due date, update dependencies 2019-10-05 06:12:36 +05:00
Maksim Eltyshev 64acf0ac56 Add secret key to environment variables, update readme 2019-10-04 04:19:46 +05:00
Maksim Eltyshev 0eef38da66 Set popup max-height, update public files, update dependencies 2019-10-03 20:07:41 +05:00
Maksim Eltyshev ded98d412b Update readme 2019-10-03 17:51:11 +05:00
Maksim Eltyshev 18f0cc59dd Markdown support in card description and comment, fixes for mobile devices, update dependencies 2019-10-03 03:02:46 +05:00
Maksim Eltyshev 530e4c26ee Fix client lint script, update dependencies 2019-10-01 18:33:52 +05:00
Maksim Eltyshev 0ff5a23b6d Fix repository language, update readme 2019-10-01 04:58:20 +05:00
Maksim Eltyshev d24c9a7968 Docker, update readme, update dependencies 2019-10-01 04:18:33 +05:00
Maksim Eltyshev 6eb431f721 Mute socket messages in production, fix max-len, update dependencies 2019-09-25 02:26:31 +05:00
Maksim Eltyshev 7bd12cd8b8 Update dependencies 2019-09-09 01:40:06 +05:00
Maksim Eltyshev 562d855d46 Add client demo 2019-09-04 22:25:24 +05:00
Maksim Eltyshev 517a508cce Fix security vulnerabilities, update dependencies 2019-09-03 01:16:09 +05:00
Maksim Eltyshev c7ffc0652a Fix dotenv path in knexfile 2019-08-31 04:43:35 +05:00
Maksim Eltyshev 5ffef61fe7 Initial commit 2019-08-31 04:07:25 +05:00
880 changed files with 142968 additions and 3337 deletions
+11
View File
@@ -0,0 +1,11 @@
*/node_modules
*/.gitignore
*/README.md
**/.DS_Store
**/.gitkeep
server/public/*
server/views/*
server/logs
client/build
+1
View File
@@ -0,0 +1 @@
client/src/lib/custom-ui/styles.css linguist-vendored
+13
View File
@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: meltyshev
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -0,0 +1,44 @@
name: Build and push Docker base image
on:
push:
paths:
- ./Dockerfile.base
branches:
- master
workflow_dispatch:
env:
ALPINE_VERSION: 3.15
jobs:
build-and-push-docker-base-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: Dockerfile.base
build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }}
push: true
tags: |
ghcr.io/plankanban/planka:base-latest
ghcr.io/plankanban/planka:base-${{ env.ALPINE_VERSION }}
@@ -0,0 +1,42 @@
name: Build and push Docker image
on:
release:
types: [created]
jobs:
build-and-push-docker-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set version
uses: actions/github-script@v6
id: set-version
with:
result-encoding: string
script: return context.payload.release.tag_name.replace('v', '')
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/plankanban/planka:latest
ghcr.io/plankanban/planka:${{ steps.set-version.outputs.result }}
+5
View File
@@ -0,0 +1,5 @@
node_modules
docker-compose.override.yml
.idea
.DS_Store
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"editor.tabSize": 2,
"editor.formatOnSave": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"eslint.format.enable": true,
"eslint.workingDirectories": [
"./client",
"./server"
],
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
+76
View File
@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at contact@plankanban.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+49
View File
@@ -0,0 +1,49 @@
# Contributing to Planka
First off, thanks for taking the time to contribute!
## Code of Conduct
This project and everyone participating in it is governed by the [Planka Code of Conduct](https://github.com/plankanban/planka/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
## How Can I Contribute?
### Reporting Bugs
Feel free to create a bug report as a new issue on GitHub. Before creating, please check if there is already existing one. When creating a bug report, please include as many details as possible.
### Suggesting Enhancements
Feel free to create an enhancement suggestion as a new issue on GitHub. Before creating, please check if there is already existing one. When creating an enhancement suggestion, please include as many details as possible.
### Pull Requests
Before submitting a pull request please discuss with the core team by creating or commenting in an issue on GitHub – we'd also love to hear from you in the discussions. This way we can ensure that an approach is agreed on before code is written. This will result in a much higher liklihood of your code being accepted.
If you’re looking for ways to get started, here's a list of ways to help us improve Planka:
- [Translation](https://github.com/plankanban/planka/issues/66) into other languages
- Issues with [`good first issue`](https://github.com/plankanban/planka/labels/good%20first%20issue) label
- Performance improvements, both on client and server
- Developer happiness and documentation
- Bugs and other issues listed on GitHub
## Styleguides
### Git Commit Messages
Commit messages should follow the [commit message convention](https://conventionalcommits.org), so changelogs could be generated automatically by that.
Additional rules:
- Separate subject from body with a blank line
- Limit the subject line to 70 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line
- Use the body to explain what and why vs. how
- Each commit should be a single, stable change
### JavaScript
All JavaScript code should follow this [JavaScript style guide](https://github.com/airbnb/javascript). The pre-commit hook will help you find linting errors before committing.
+37
View File
@@ -0,0 +1,37 @@
FROM node:lts AS client-builder
WORKDIR /app
COPY client/package.json client/package-lock.json ./
RUN npm install npm@latest --global \
&& npm install
COPY client .
RUN npm run build
FROM ghcr.io/plankanban/planka:base-latest
WORKDIR /app
COPY server/.npmrc server/package.json server/package-lock.json ./
RUN npm install npm@latest --global \
&& npm install --production \
&& apk del vips-dependencies --purge
COPY docker-start.sh start.sh
COPY server .
RUN cp .env.sample .env
COPY --from=client-builder /app/build public
COPY --from=client-builder /app/build/index.html views
VOLUME /app/public/user-avatars
VOLUME /app/public/project-background-images
VOLUME /app/private/attachments
EXPOSE 1337
CMD ["./start.sh"]
+27
View File
@@ -0,0 +1,27 @@
FROM node:lts-alpine
ARG ALPINE_VERSION=3.15
ARG VIPS_VERSION=8.12.2
RUN apk -U upgrade \
&& apk add \
bash giflib glib lcms2 libexif \
libgsf libjpeg-turbo libpng librsvg libwebp \
orc pango tiff \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \
&& apk add \
build-base giflib-dev glib-dev lcms2-dev libexif-dev \
libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
orc-dev pango-dev tiff-dev \
--virtual vips-dependencies \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
&& cd /tmp/vips-${VIPS_VERSION} \
&& ./configure \
&& make \
&& make install-strip \
&& rm -rf /tmp/vips-${VIPS_VERSION}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Maksim Eltyshev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+159
View File
@@ -0,0 +1,159 @@
# Planka
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/docker/pulls/meltyshev/planka) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
A Trello-like kanban board built with React and Redux.
![](https://raw.githubusercontent.com/plankanban/planka/master/demo.gif)
[**Client demo**](https://plankanban.github.io/planka) (without server features).
## Features
- Create projects, boards, lists, cards, labels and tasks
- Add card members, track time, set a due date, add attachments, write comments
- Markdown support in a card description and comment
- Filter by members and labels
- Customize project background
- Real-time updates
- User notifications
- Internationalization
## Deploy
### Docker Compose
[![](https://d207aa93qlcgug.cloudfront.net/1.95.5.qa/img/nav/docker-logo-loggedout.png)](https://hub.docker.com/r/meltyshev/planka)
- Make sure you have [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) installed and operational.
- Create `docker-compose.yml` based on [the example](https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml). This is the ONLY file you will need. You can create this file on your own machine by copy and pasting the content.
- Edit `BASE_URL` to match your domain name or IP address.
- Edit `SECRET_KEY` with random value. You can generate it by `openssl rand -hex 64`.
Download the docker-compose.yml:
```
curl -L https://raw.githubusercontent.com/plankanban/planka/master/docker-compose.yml -o docker-compose.yml
```
Pull images and start services:
```
docker-compose up -d
```
Demo user: demo@demo.demo demo
### Logging
Planka currently allows you to expose the applciation's logfile directory to the host machine via a shared volume. This feature is not enabled by default.
To expose the logfile director to the host machine, add the item `./logs/:/app/logs/` under `services.planka.volumes`.
Note that the directory to the left of the semicolon is regarding the host machine while the directory to the right of the semicolon is regarding the Docker container.
For example, in the above step, `./logs/:/app/logs/` will create the folder `logs` in the same directory where the `docker-compose.yml` file lives.
### Rotating Logs
Logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large.
#### Setup logrotate for Planka logs
Create a file in `/etc/logrotate.d` named `planka` with the following contents:
```
/path/to/planka/logs/planka.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 640 root adm
sharedscripts
}
```
Ensure to replace logfile directory with your installation’s `/logs/planka.log` location.
Restart the logrotate service.
### Fail2ban
Fail2ban is a service that uses iptables to automatically drop connections for a pre-defined amount of time from IPs that continuously failed to authenticate to the configured services.
#### Setup a filter and a jail for Planka
A filter defines regex rules to identify when users fail to authenticate on Planka's user interface.
Create a file in `/etc/fail2ban/filter.d` named `planka.conf` with the following contents:
```conf
[Definition]
failregex = ^(.*) Invalid (email or username:|password!) (\"(.*)\"!)? ?\(IP: <ADDR>\)$
ignoreregex =
```
The jail file defines how to handle the failed authentication attempts found by the Planka filter.
Create a file in `/etc/fail2ban/jail.d` named `planka.local` with the following contents:
```conf
[planka]
enabled = true
port = http,https
filter = planka
logpath = /path/to/planka/logs/planka.log
maxretry = 5
bantime = 900
```
Ensure to replace `logpath`'s value with your installation’s `/logs/planka.log` location. If you are using ports other than 80 and 443 for your Web server you should replace those too. The bantime and findtime are defined in seconds.
Restart the fail2ban service. You can check the status of your Planka jail by running:
```bash
fail2ban-client status planka
```
## Development
Clone the repository and install dependencies:
```
git clone https://github.com/plankanban/planka.git
cd planka
npm install
```
Either use a local database or start the provided development database:
```
docker-compose -f docker-compose-dev.yml up
```
Create `server/.env` based on `server/.env.sample` and edit `DATABASE_URL` if needed, then initialize the database:
```
npm run server:db:init
```
Start the development server:
```
npm start
```
Demo user: demo@demo.demo demo
## Tech stack
- React, Redux, Redux-Saga, Redux-ORM, Semantic UI React, react-beautiful-dnd
- Sails.js, Knex.js
- PostgreSQL
## License
Planka is [MIT licensed](https://github.com/plankanban/planka/blob/master/LICENSE).
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
Most recent release.
## Reporting a Vulnerability
Please report any security issues you discovered to meltyshev@gmail.com. If the issue is confirmed, we will release a patch as soon as possible depending on complexity.
**Do NOT create public issues on GitHub for security vulnerabilities.**
Thank you for your contribution!
-80
View File
@@ -1,80 +0,0 @@
{
"files": {
"main.css": "/planka/static/css/main.859cd79e.css",
"main.js": "/planka/static/js/main.5081d4cc.js",
"static/js/801.9773e58e.chunk.js": "/planka/static/js/801.9773e58e.chunk.js",
"static/js/467.d02e66c6.chunk.js": "/planka/static/js/467.d02e66c6.chunk.js",
"static/js/514.592fb053.chunk.js": "/planka/static/js/514.592fb053.chunk.js",
"static/js/294.e0243dff.chunk.js": "/planka/static/js/294.e0243dff.chunk.js",
"static/js/394.5692e060.chunk.js": "/planka/static/js/394.5692e060.chunk.js",
"static/js/117.2c68642f.chunk.js": "/planka/static/js/117.2c68642f.chunk.js",
"static/js/181.24dbd8ba.chunk.js": "/planka/static/js/181.24dbd8ba.chunk.js",
"static/js/902.1af73487.chunk.js": "/planka/static/js/902.1af73487.chunk.js",
"static/js/875.4756cbe5.chunk.js": "/planka/static/js/875.4756cbe5.chunk.js",
"static/js/694.e5cf50a6.chunk.js": "/planka/static/js/694.e5cf50a6.chunk.js",
"static/js/632.9315886b.chunk.js": "/planka/static/js/632.9315886b.chunk.js",
"static/js/812.77d21c8d.chunk.js": "/planka/static/js/812.77d21c8d.chunk.js",
"static/js/215.bb7d98af.chunk.js": "/planka/static/js/215.bb7d98af.chunk.js",
"static/js/683.e8a7c172.chunk.js": "/planka/static/js/683.e8a7c172.chunk.js",
"static/media/big-picture.jpg": "/planka/static/media/big-picture.b78130bf7100f3416c1c.jpg",
"static/media/cover.jpg": "/planka/static/media/cover.a3c21cab987867165c62.jpg",
"static/media/simple-picture.jpg": "/planka/static/media/simple-picture.0442003259f074c6cc5d.jpg",
"static/media/brand-icons.svg": "/planka/static/media/brand-icons.6729d29753e000c17489.svg",
"static/media/simple-pdf.pdf": "/planka/static/media/simple-pdf.2b466d228796d7c86d15.pdf",
"static/media/fontawesome-webfont.svg?v=4.7.0": "/planka/static/media/fontawesome-webfont.c1e38fd9e0e74ba58f7a.svg",
"static/media/icons.svg": "/planka/static/media/icons.62d9dae4e0040e81c980.svg",
"static/media/original.jpg": "/planka/static/media/original.fd3fb2a1d11429b0ec28.jpg",
"static/media/high-picture.jpg": "/planka/static/media/high-picture.28d94c085a2a9ac5ccf3.jpg",
"static/media/fontawesome-webfont.eot": "/planka/static/media/fontawesome-webfont.8b43027f47b20503057d.eot",
"static/media/fontawesome-webfont.ttf?v=4.7.0": "/planka/static/media/fontawesome-webfont.1e59d2330b4c6deb84b3.ttf",
"static/media/outline-icons.svg": "/planka/static/media/outline-icons.9c4845b4b41ef40a22fa.svg",
"static/media/icons.eot": "/planka/static/media/icons.a01e3f2d6c83dc3aee17.eot",
"static/media/icons.ttf": "/planka/static/media/icons.c656b8caa454ed19b9a2.ttf",
"static/media/cover-256.jpg": "/planka/static/media/cover-256.4acd3319c6762c5d1b4b.jpg",
"static/media/brand-icons.eot": "/planka/static/media/brand-icons.d68fa3e67dbb653a13ce.eot",
"static/media/brand-icons.ttf": "/planka/static/media/brand-icons.65a2fb6d9aaa164b41a0.ttf",
"static/media/fontawesome-webfont.woff?v=4.7.0": "/planka/static/media/fontawesome-webfont.f691f37e57f04c152e23.woff",
"static/media/cover-336.jpg": "/planka/static/media/cover-336.d1a186776f06837a4ec3.jpg",
"static/media/fontawesome-webfont.woff2?v=4.7.0": "/planka/static/media/fontawesome-webfont.20fd1704ea223900efa9.woff2",
"static/media/brand-icons.woff": "/planka/static/media/brand-icons.cac87dc00c87a5d74711.woff",
"static/media/brand-icons.woff2": "/planka/static/media/brand-icons.278156e41e0ad908cf7f.woff2",
"static/media/marked.cjs": "/planka/static/media/marked.2244abd198e53bc0b3d6.cjs",
"static/media/icons.woff": "/planka/static/media/icons.425399f81e4ce7cbd967.woff",
"static/media/Nunitoga-Medium.woff2": "/planka/static/media/Nunitoga-Medium.bae369ea6c0c9ad31db6.woff2",
"static/media/Nunitoga-BoldItalic.woff2": "/planka/static/media/Nunitoga-BoldItalic.06ec745f47d8c9431b42.woff2",
"static/media/Nunitoga-MediumItalic.woff2": "/planka/static/media/Nunitoga-MediumItalic.5ce0915188332f04e46d.woff2",
"static/media/Nunitoga-LightItalic.woff2": "/planka/static/media/Nunitoga-LightItalic.f1b251d4faa9826b3ea2.woff2",
"static/media/Nunitoga-Bold.woff2": "/planka/static/media/Nunitoga-Bold.46fd9a43c89bf2b6a1fe.woff2",
"static/media/Nunitoga-Light.woff2": "/planka/static/media/Nunitoga-Light.e9d89c979b64722f34ec.woff2",
"static/media/icons.woff2": "/planka/static/media/icons.38c6d8bab26db77d8c80.woff2",
"static/media/outline-icons.eot": "/planka/static/media/outline-icons.752905fa5edf21fc52a1.eot",
"static/media/outline-icons.ttf": "/planka/static/media/outline-icons.5367103510b27b784827.ttf",
"static/media/flags.png": "/planka/static/media/flags.99f63ae7a743f21ab308.png",
"static/media/logo.png": "/planka/static/media/logo.f8c2b962d0f2bd7f46cb.png",
"static/media/outline-icons.woff": "/planka/static/media/outline-icons.ddae9b1ba9b0b42f5880.woff",
"static/media/outline-icons.woff2": "/planka/static/media/outline-icons.687a4990ea22bb1a49d4.woff2",
"index.html": "/planka/index.html",
"static/media/plus-icon.svg": "/planka/static/media/plus-icon.e46c682424b8c98c4f7bd0f367a260c2.svg",
"static/media/plus-math-icon.svg": "/planka/static/media/plus-math-icon.cfa58840e75860dcf79a0b21a1cca781.svg",
"main.859cd79e.css.map": "/planka/static/css/main.859cd79e.css.map",
"main.5081d4cc.js.map": "/planka/static/js/main.5081d4cc.js.map",
"801.9773e58e.chunk.js.map": "/planka/static/js/801.9773e58e.chunk.js.map",
"467.d02e66c6.chunk.js.map": "/planka/static/js/467.d02e66c6.chunk.js.map",
"514.592fb053.chunk.js.map": "/planka/static/js/514.592fb053.chunk.js.map",
"294.e0243dff.chunk.js.map": "/planka/static/js/294.e0243dff.chunk.js.map",
"394.5692e060.chunk.js.map": "/planka/static/js/394.5692e060.chunk.js.map",
"117.2c68642f.chunk.js.map": "/planka/static/js/117.2c68642f.chunk.js.map",
"181.24dbd8ba.chunk.js.map": "/planka/static/js/181.24dbd8ba.chunk.js.map",
"902.1af73487.chunk.js.map": "/planka/static/js/902.1af73487.chunk.js.map",
"875.4756cbe5.chunk.js.map": "/planka/static/js/875.4756cbe5.chunk.js.map",
"694.e5cf50a6.chunk.js.map": "/planka/static/js/694.e5cf50a6.chunk.js.map",
"632.9315886b.chunk.js.map": "/planka/static/js/632.9315886b.chunk.js.map",
"812.77d21c8d.chunk.js.map": "/planka/static/js/812.77d21c8d.chunk.js.map",
"215.bb7d98af.chunk.js.map": "/planka/static/js/215.bb7d98af.chunk.js.map",
"683.e8a7c172.chunk.js.map": "/planka/static/js/683.e8a7c172.chunk.js.map"
},
"entrypoints": [
"static/css/main.859cd79e.css",
"static/js/main.5081d4cc.js"
]
}
+23
View File
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+1
View File
@@ -0,0 +1 @@
# Planka client
+43810
View File
File diff suppressed because it is too large Load Diff
+123
View File
@@ -0,0 +1,123 @@
{
"name": "planka-client",
"private": true,
"scripts": {
"build": "react-scripts build",
"eject": "react-scripts eject",
"lint": "eslint --ext js,jsx src",
"start": "react-scripts start",
"test": "react-scripts test"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"eslintConfig": {
"env": {
"browser": true,
"jest": true
},
"parser": "@babel/eslint-parser",
"parserOptions": {
"babelOptions": {
"presets": [
"airbnb"
]
},
"requireConfigFile": false
},
"plugins": [
"prettier"
],
"extends": [
"airbnb",
"airbnb/hooks",
"plugin:prettier/recommended"
],
"rules": {
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"src/setupTests.js",
"**/*.test.js"
]
}
],
"prettier/prettier": [
"error",
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all"
}
]
}
},
"dependencies": {
"classnames": "^2.3.1",
"connected-react-router": "^6.9.3",
"date-fns": "^2.29.1",
"dequal": "^2.0.3",
"history": "^4.10.1",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
"initials": "^3.1.2",
"js-cookie": "^3.0.1",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"node-sass": "^7.0.1",
"photoswipe": "^5.3.0",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-datepicker": "^4.8.0",
"react-dom": "^17.0.2",
"react-dropzone": "^14.2.2",
"react-i18next": "^11.18.1",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.3",
"react-photoswipe-gallery": "^2.2.1",
"react-redux": "^7.2.8",
"react-router-dom": "^5.3.1",
"react-scripts": "5.0.1",
"react-textarea-autosize": "^8.3.4",
"redux": "^4.2.0",
"redux-logger": "^3.0.6",
"redux-orm": "^0.16.2",
"redux-saga": "^1.1.3",
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"reselect": "^4.1.6",
"sails.io.js": "^1.2.1",
"semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.3.1",
"validator": "^13.7.0",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
"babel-preset-airbnb": "^5.0.0",
"chai": "^4.3.6",
"enzyme": "^3.11.0",
"eslint": "^8.20.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"jest-enzyme": "^7.1.2",
"prettier": "2.7.1",
"react-test-renderer": "^17.0.2"
}
}

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Planka is an open source project management software"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Planka</title>
</head>
<body id="app">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

+79
View File
@@ -0,0 +1,79 @@
import ActionTypes from '../constants/ActionTypes';
const fetchActivities = (cardId) => ({
type: ActionTypes.ACTIVITIES_FETCH,
payload: {
cardId,
},
});
fetchActivities.success = (cardId, activities, users) => ({
type: ActionTypes.ACTIVITIES_FETCH__SUCCESS,
payload: {
cardId,
activities,
users,
},
});
fetchActivities.failure = (cardId, error) => ({
type: ActionTypes.ACTIVITIES_FETCH__FAILURE,
payload: {
cardId,
error,
},
});
const toggleActivitiesDetails = (cardId, isVisible) => ({
type: ActionTypes.ACTIVITIES_DETAILS_TOGGLE,
payload: {
cardId,
isVisible,
},
});
toggleActivitiesDetails.success = (cardId, activities, users) => ({
type: ActionTypes.ACTIVITIES_DETAILS_TOGGLE__SUCCESS,
payload: {
cardId,
activities,
users,
},
});
toggleActivitiesDetails.failure = (cardId, error) => ({
type: ActionTypes.ACTIVITIES_DETAILS_TOGGLE__FAILURE,
payload: {
cardId,
error,
},
});
const handleActivityCreate = (activity) => ({
type: ActionTypes.ACTIVITY_CREATE_HANDLE,
payload: {
activity,
},
});
const handleActivityUpdate = (activity) => ({
type: ActionTypes.ACTIVITY_UPDATE_HANDLE,
payload: {
activity,
},
});
const handleActivityDelete = (activity) => ({
type: ActionTypes.ACTIVITY_DELETE_HANDLE,
payload: {
activity,
},
});
export default {
fetchActivities,
toggleActivitiesDetails,
handleActivityCreate,
handleActivityUpdate,
handleActivityDelete,
};
+99
View File
@@ -0,0 +1,99 @@
import ActionTypes from '../constants/ActionTypes';
const createAttachment = (attachment) => ({
type: ActionTypes.ATTACHMENT_CREATE,
payload: {
attachment,
},
});
createAttachment.success = (localId, attachment) => ({
type: ActionTypes.ATTACHMENT_CREATE__SUCCESS,
payload: {
localId,
attachment,
},
});
createAttachment.failure = (localId, error) => ({
type: ActionTypes.ATTACHMENT_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleAttachmentCreate = (attachment) => ({
type: ActionTypes.ATTACHMENT_CREATE_HANDLE,
payload: {
attachment,
},
});
const updateAttachment = (id, data) => ({
type: ActionTypes.ATTACHMENT_UPDATE,
payload: {
id,
data,
},
});
updateAttachment.success = (attachment) => ({
type: ActionTypes.ATTACHMENT_UPDATE__SUCCESS,
payload: {
attachment,
},
});
updateAttachment.failure = (id, error) => ({
type: ActionTypes.ATTACHMENT_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleAttachmentUpdate = (attachment) => ({
type: ActionTypes.ATTACHMENT_UPDATE_HANDLE,
payload: {
attachment,
},
});
const deleteAttachment = (id) => ({
type: ActionTypes.ATTACHMENT_DELETE,
payload: {
id,
},
});
deleteAttachment.success = (attachment) => ({
type: ActionTypes.ATTACHMENT_DELETE__SUCCESS,
payload: {
attachment,
},
});
deleteAttachment.failure = (id, error) => ({
type: ActionTypes.ATTACHMENT_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleAttachmentDelete = (attachment) => ({
type: ActionTypes.ATTACHMENT_DELETE_HANDLE,
payload: {
attachment,
},
});
export default {
createAttachment,
handleAttachmentCreate,
updateAttachment,
handleAttachmentUpdate,
deleteAttachment,
handleAttachmentDelete,
};
+138
View File
@@ -0,0 +1,138 @@
import ActionTypes from '../constants/ActionTypes';
const createBoardMembership = (boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_CREATE,
payload: {
boardMembership,
},
});
createBoardMembership.success = (localId, boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_CREATE__SUCCESS,
payload: {
localId,
boardMembership,
},
});
createBoardMembership.failure = (localId, error) => ({
type: ActionTypes.BOARD_MEMBERSHIP_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleBoardMembershipCreate = (
boardMembership,
project,
board,
users,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
deletedNotifications,
) => ({
type: ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE,
payload: {
boardMembership,
project,
board,
users,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
deletedNotifications,
},
});
handleBoardMembershipCreate.fetchProject = (id, currentUserId, currentBoardId) => ({
type: ActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH,
payload: {
id,
currentUserId,
currentBoardId,
},
});
const updateBoardMembership = (id, data) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE,
payload: {
id,
data,
},
});
updateBoardMembership.success = (boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS,
payload: {
boardMembership,
},
});
updateBoardMembership.failure = (id, error) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleBoardMembershipUpdate = (boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE,
payload: {
boardMembership,
},
});
const deleteBoardMembership = (id) => ({
type: ActionTypes.BOARD_MEMBERSHIP_DELETE,
payload: {
id,
},
});
deleteBoardMembership.success = (boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_DELETE__SUCCESS,
payload: {
boardMembership,
},
});
deleteBoardMembership.failure = (id, error) => ({
type: ActionTypes.BOARD_MEMBERSHIP_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleBoardMembershipDelete = (boardMembership) => ({
type: ActionTypes.BOARD_MEMBERSHIP_DELETE_HANDLE,
payload: {
boardMembership,
},
});
export default {
createBoardMembership,
handleBoardMembershipCreate,
updateBoardMembership,
handleBoardMembershipUpdate,
deleteBoardMembership,
handleBoardMembershipDelete,
};
+145
View File
@@ -0,0 +1,145 @@
import ActionTypes from '../constants/ActionTypes';
const createBoard = (board) => ({
type: ActionTypes.BOARD_CREATE,
payload: {
board,
},
});
createBoard.success = (localId, board, boardMemberships) => ({
type: ActionTypes.BOARD_CREATE__SUCCESS,
payload: {
localId,
board,
boardMemberships,
},
});
createBoard.failure = (localId, error) => ({
type: ActionTypes.BOARD_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleBoardCreate = (board) => ({
type: ActionTypes.BOARD_CREATE_HANDLE,
payload: {
board,
},
});
const fetchBoard = (id) => ({
type: ActionTypes.BOARD_FETCH,
payload: {
id,
},
});
fetchBoard.success = (
board,
users,
projects,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
) => ({
type: ActionTypes.BOARD_FETCH__SUCCESS,
payload: {
board,
users,
projects,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
},
});
fetchBoard.failure = (id, error) => ({
type: ActionTypes.BOARD_FETCH__FAILURE,
payload: {
id,
error,
},
});
const updateBoard = (id, data) => ({
type: ActionTypes.BOARD_UPDATE,
payload: {
id,
data,
},
});
updateBoard.success = (board) => ({
type: ActionTypes.BOARD_UPDATE__SUCCESS,
payload: {
board,
},
});
updateBoard.failure = (id, error) => ({
type: ActionTypes.BOARD_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleBoardUpdate = (board) => ({
type: ActionTypes.BOARD_UPDATE_HANDLE,
payload: {
board,
},
});
const deleteBoard = (id) => ({
type: ActionTypes.BOARD_DELETE,
payload: {
id,
},
});
deleteBoard.success = (board) => ({
type: ActionTypes.BOARD_DELETE__SUCCESS,
payload: {
board,
},
});
deleteBoard.failure = (id, error) => ({
type: ActionTypes.BOARD_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleBoardDelete = (board) => ({
type: ActionTypes.BOARD_DELETE_HANDLE,
payload: {
board,
},
});
export default {
createBoard,
handleBoardCreate,
fetchBoard,
updateBoard,
handleBoardUpdate,
deleteBoard,
handleBoardDelete,
};
+99
View File
@@ -0,0 +1,99 @@
import ActionTypes from '../constants/ActionTypes';
const createCard = (card) => ({
type: ActionTypes.CARD_CREATE,
payload: {
card,
},
});
createCard.success = (localId, card) => ({
type: ActionTypes.CARD_CREATE__SUCCESS,
payload: {
localId,
card,
},
});
createCard.failure = (localId, error) => ({
type: ActionTypes.CARD_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleCardCreate = (card) => ({
type: ActionTypes.CARD_CREATE_HANDLE,
payload: {
card,
},
});
const updateCard = (id, data) => ({
type: ActionTypes.CARD_UPDATE,
payload: {
id,
data,
},
});
updateCard.success = (card) => ({
type: ActionTypes.CARD_UPDATE__SUCCESS,
payload: {
card,
},
});
updateCard.failure = (id, error) => ({
type: ActionTypes.CARD_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleCardUpdate = (card) => ({
type: ActionTypes.CARD_UPDATE_HANDLE,
payload: {
card,
},
});
const deleteCard = (id) => ({
type: ActionTypes.CARD_DELETE,
payload: {
id,
},
});
deleteCard.success = (card) => ({
type: ActionTypes.CARD_DELETE__SUCCESS,
payload: {
card,
},
});
deleteCard.failure = (id, error) => ({
type: ActionTypes.CARD_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleCardDelete = (card) => ({
type: ActionTypes.CARD_DELETE_HANDLE,
payload: {
card,
},
});
export default {
createCard,
handleCardCreate,
updateCard,
handleCardUpdate,
deleteCard,
handleCardDelete,
};
+75
View File
@@ -0,0 +1,75 @@
import ActionTypes from '../constants/ActionTypes';
const createCommentActivity = (activity) => ({
type: ActionTypes.COMMENT_ACTIVITY_CREATE,
payload: {
activity,
},
});
createCommentActivity.success = (localId, activity) => ({
type: ActionTypes.COMMENT_ACTIVITY_CREATE__SUCCESS,
payload: {
localId,
activity,
},
});
createCommentActivity.failure = (localId, error) => ({
type: ActionTypes.COMMENT_ACTIVITY_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const updateCommentActivity = (id, data) => ({
type: ActionTypes.COMMENT_ACTIVITY_UPDATE,
payload: {
id,
data,
},
});
updateCommentActivity.success = (activity) => ({
type: ActionTypes.COMMENT_ACTIVITY_UPDATE__SUCCESS,
payload: {
activity,
},
});
updateCommentActivity.failure = (id, error) => ({
type: ActionTypes.COMMENT_ACTIVITY_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const deleteCommentActivity = (id) => ({
type: ActionTypes.COMMENT_ACTIVITY_DELETE,
payload: {
id,
},
});
deleteCommentActivity.success = (activity) => ({
type: ActionTypes.COMMENT_ACTIVITY_DELETE__SUCCESS,
payload: {
activity,
},
});
deleteCommentActivity.failure = (id, error) => ({
type: ActionTypes.COMMENT_ACTIVITY_DELETE__FAILURE,
payload: {
id,
error,
},
});
export default {
createCommentActivity,
updateCommentActivity,
deleteCommentActivity,
};
+50
View File
@@ -0,0 +1,50 @@
import ActionTypes from '../constants/ActionTypes';
const initializeCore = (
user,
board,
users,
projects,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
activities,
notifications,
) => ({
type: ActionTypes.CORE_INITIALIZE,
payload: {
user,
board,
users,
projects,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
activities,
notifications,
},
});
const logout = () => ({
type: ActionTypes.LOGOUT,
payload: {},
});
export default {
initializeCore,
logout,
};
+39
View File
@@ -0,0 +1,39 @@
import router from './router';
import socket from './socket';
import login from './login';
import core from './core';
import modals from './modals';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
import boards from './boards';
import boardMemberships from './board-memberships';
import labels from './labels';
import lists from './lists';
import cards from './cards';
import tasks from './tasks';
import attachments from './attachments';
import activities from './activities';
import commentActivities from './comment-activities';
import notifications from './notifications';
export default {
...router,
...socket,
...login,
...core,
...modals,
...users,
...projects,
...projectManagers,
...boards,
...boardMemberships,
...labels,
...lists,
...cards,
...tasks,
...attachments,
...activities,
...commentActivities,
...notifications,
};
+183
View File
@@ -0,0 +1,183 @@
import ActionTypes from '../constants/ActionTypes';
const createLabel = (label) => ({
type: ActionTypes.LABEL_CREATE,
payload: {
label,
},
});
createLabel.success = (localId, label) => ({
type: ActionTypes.LABEL_CREATE__SUCCESS,
payload: {
localId,
label,
},
});
createLabel.failure = (localId, error) => ({
type: ActionTypes.LABEL_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleLabelCreate = (label) => ({
type: ActionTypes.LABEL_CREATE_HANDLE,
payload: {
label,
},
});
const updateLabel = (id, data) => ({
type: ActionTypes.LABEL_UPDATE,
payload: {
id,
data,
},
});
updateLabel.success = (label) => ({
type: ActionTypes.LABEL_UPDATE__SUCCESS,
payload: {
label,
},
});
updateLabel.failure = (id, error) => ({
type: ActionTypes.LABEL_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleLabelUpdate = (label) => ({
type: ActionTypes.LABEL_UPDATE_HANDLE,
payload: {
label,
},
});
const deleteLabel = (id) => ({
type: ActionTypes.LABEL_DELETE,
payload: {
id,
},
});
deleteLabel.success = (label) => ({
type: ActionTypes.LABEL_DELETE__SUCCESS,
payload: {
label,
},
});
deleteLabel.failure = (id, error) => ({
type: ActionTypes.LABEL_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleLabelDelete = (label) => ({
type: ActionTypes.LABEL_DELETE_HANDLE,
payload: {
label,
},
});
const addLabelToCard = (id, cardId) => ({
type: ActionTypes.LABEL_TO_CARD_ADD,
payload: {
id,
cardId,
},
});
addLabelToCard.success = (cardLabel) => ({
type: ActionTypes.LABEL_TO_CARD_ADD__SUCCESS,
payload: {
cardLabel,
},
});
addLabelToCard.failure = (id, cardId, error) => ({
type: ActionTypes.LABEL_TO_CARD_ADD__FAILURE,
payload: {
id,
cardId,
error,
},
});
const handleLabelToCardAdd = (cardLabel) => ({
type: ActionTypes.LABEL_TO_CARD_ADD_HANDLE,
payload: {
cardLabel,
},
});
const removeLabelFromCard = (id, cardId) => ({
type: ActionTypes.LABEL_FROM_CARD_REMOVE,
payload: {
id,
cardId,
},
});
removeLabelFromCard.success = (cardLabel) => ({
type: ActionTypes.LABEL_FROM_CARD_REMOVE__SUCCESS,
payload: {
cardLabel,
},
});
removeLabelFromCard.failure = (id, cardId, error) => ({
type: ActionTypes.LABEL_FROM_CARD_REMOVE__FAILURE,
payload: {
id,
cardId,
error,
},
});
const handleLabelFromCardRemove = (cardLabel) => ({
type: ActionTypes.LABEL_FROM_CARD_REMOVE_HANDLE,
payload: {
cardLabel,
},
});
const addLabelToBoardFilter = (id, boardId) => ({
type: ActionTypes.LABEL_TO_BOARD_FILTER_ADD,
payload: {
id,
boardId,
},
});
const removeLabelFromBoardFilter = (id, boardId) => ({
type: ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE,
payload: {
id,
boardId,
},
});
export default {
createLabel,
handleLabelCreate,
updateLabel,
handleLabelUpdate,
deleteLabel,
handleLabelDelete,
addLabelToCard,
handleLabelToCardAdd,
removeLabelFromCard,
handleLabelFromCardRemove,
addLabelToBoardFilter,
removeLabelFromBoardFilter,
};
+99
View File
@@ -0,0 +1,99 @@
import ActionTypes from '../constants/ActionTypes';
const createList = (list) => ({
type: ActionTypes.LIST_CREATE,
payload: {
list,
},
});
createList.success = (localId, list) => ({
type: ActionTypes.LIST_CREATE__SUCCESS,
payload: {
localId,
list,
},
});
createList.failure = (localId, error) => ({
type: ActionTypes.LIST_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleListCreate = (list) => ({
type: ActionTypes.LIST_CREATE_HANDLE,
payload: {
list,
},
});
const updateList = (id, data) => ({
type: ActionTypes.LIST_UPDATE,
payload: {
id,
data,
},
});
updateList.success = (list) => ({
type: ActionTypes.LIST_UPDATE__SUCCESS,
payload: {
list,
},
});
updateList.failure = (id, error) => ({
type: ActionTypes.LIST_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleListUpdate = (list) => ({
type: ActionTypes.LIST_UPDATE_HANDLE,
payload: {
list,
},
});
const deleteList = (id) => ({
type: ActionTypes.LIST_DELETE,
payload: {
id,
},
});
deleteList.success = (list) => ({
type: ActionTypes.LIST_DELETE__SUCCESS,
payload: {
list,
},
});
deleteList.failure = (id, error) => ({
type: ActionTypes.LIST_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleListDelete = (list) => ({
type: ActionTypes.LIST_DELETE_HANDLE,
payload: {
list,
},
});
export default {
createList,
handleListCreate,
updateList,
handleListUpdate,
deleteList,
handleListDelete,
};
+32
View File
@@ -0,0 +1,32 @@
import ActionTypes from '../constants/ActionTypes';
const authenticate = (data) => ({
type: ActionTypes.AUTHENTICATE,
payload: {
data,
},
});
authenticate.success = (accessToken) => ({
type: ActionTypes.AUTHENTICATE__SUCCESS,
payload: {
accessToken,
},
});
authenticate.failure = (error) => ({
type: ActionTypes.AUTHENTICATE__FAILURE,
payload: {
error,
},
});
const clearAuthenticateError = () => ({
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {},
});
export default {
authenticate,
clearAuthenticateError,
};
+18
View File
@@ -0,0 +1,18 @@
import ActionTypes from '../constants/ActionTypes';
const openModal = (type) => ({
type: ActionTypes.MODAL_OPEN,
payload: {
type,
},
});
const closeModal = () => ({
type: ActionTypes.MODAL_CLOSE,
payload: {},
});
export default {
openModal,
closeModal,
};
+46
View File
@@ -0,0 +1,46 @@
import ActionTypes from '../constants/ActionTypes';
const handleNotificationCreate = (notification, users, cards, activities) => ({
type: ActionTypes.NOTIFICATION_CREATE_HANDLE,
payload: {
notification,
users,
cards,
activities,
},
});
const deleteNotification = (id) => ({
type: ActionTypes.NOTIFICATION_DELETE,
payload: {
id,
},
});
deleteNotification.success = (notification) => ({
type: ActionTypes.NOTIFICATION_DELETE__SUCCESS,
payload: {
notification,
},
});
deleteNotification.failure = (id, error) => ({
type: ActionTypes.NOTIFICATION_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleNotificationDelete = (notification) => ({
type: ActionTypes.NOTIFICATION_DELETE_HANDLE,
payload: {
notification,
},
});
export default {
handleNotificationCreate,
deleteNotification,
handleNotificationDelete,
};
+110
View File
@@ -0,0 +1,110 @@
import ActionTypes from '../constants/ActionTypes';
const createProjectManager = (projectManager) => ({
type: ActionTypes.PROJECT_MANAGER_CREATE,
payload: {
projectManager,
},
});
createProjectManager.success = (localId, projectManager) => ({
type: ActionTypes.PROJECT_MANAGER_CREATE__SUCCESS,
payload: {
localId,
projectManager,
},
});
createProjectManager.failure = (localId, error) => ({
type: ActionTypes.PROJECT_MANAGER_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleProjectManagerCreate = (
projectManager,
project,
board,
users,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
deletedNotifications,
) => ({
type: ActionTypes.PROJECT_MANAGER_CREATE_HANDLE,
payload: {
projectManager,
project,
board,
users,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
deletedNotifications,
},
});
handleProjectManagerCreate.fetchProject = (id, currentUserId, currentBoardId) => ({
type: ActionTypes.PROJECT_MANAGER_CREATE_HANDLE__PROJECT_FETCH,
payload: {
id,
currentUserId,
currentBoardId,
},
});
const deleteProjectManager = (id, isCurrentUser, isCurrentProject) => ({
type: ActionTypes.PROJECT_MANAGER_DELETE,
payload: {
id,
isCurrentUser,
isCurrentProject,
},
});
deleteProjectManager.success = (projectManager) => ({
type: ActionTypes.PROJECT_MANAGER_DELETE__SUCCESS,
payload: {
projectManager,
},
});
deleteProjectManager.failure = (id, error) => ({
type: ActionTypes.PROJECT_MANAGER_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleProjectManagerDelete = (projectManager, isCurrentUser, isCurrentProject) => ({
type: ActionTypes.PROJECT_MANAGER_DELETE_HANDLE,
payload: {
projectManager,
isCurrentUser,
isCurrentProject,
},
});
export default {
createProjectManager,
handleProjectManagerCreate,
deleteProjectManager,
handleProjectManagerDelete,
};
+125
View File
@@ -0,0 +1,125 @@
import ActionTypes from '../constants/ActionTypes';
const createProject = (data) => ({
type: ActionTypes.PROJECT_CREATE,
payload: {
data,
},
});
createProject.success = (project, projectManagers) => ({
type: ActionTypes.PROJECT_CREATE__SUCCESS,
payload: {
project,
projectManagers,
},
});
createProject.failure = (error) => ({
type: ActionTypes.PROJECT_CREATE__FAILURE,
payload: {
error,
},
});
const handleProjectCreate = (project, users, projectManagers, boards, boardMemberships) => ({
type: ActionTypes.PROJECT_CREATE_HANDLE,
payload: {
project,
users,
projectManagers,
boards,
boardMemberships,
},
});
const updateProject = (id, data) => ({
type: ActionTypes.PROJECT_UPDATE,
payload: {
id,
data,
},
});
updateProject.success = (project) => ({
type: ActionTypes.PROJECT_UPDATE__SUCCESS,
payload: {
project,
},
});
updateProject.failure = (id, error) => ({
type: ActionTypes.PROJECT_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleProjectUpdate = (project) => ({
type: ActionTypes.PROJECT_UPDATE_HANDLE,
payload: {
project,
},
});
const updateProjectBackgroundImage = (id) => ({
type: ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE,
payload: {
id,
},
});
updateProjectBackgroundImage.success = (project) => ({
type: ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE__SUCCESS,
payload: {
project,
},
});
updateProjectBackgroundImage.failure = (id, error) => ({
type: ActionTypes.PROJECT_BACKGROUND_IMAGE_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const deleteProject = (id) => ({
type: ActionTypes.PROJECT_DELETE,
payload: {
id,
},
});
deleteProject.success = (project) => ({
type: ActionTypes.PROJECT_DELETE__SUCCESS,
payload: {
project,
},
});
deleteProject.failure = (id, error) => ({
type: ActionTypes.PROJECT_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleProjectDelete = (project) => ({
type: ActionTypes.PROJECT_DELETE_HANDLE,
payload: {
project,
},
});
export default {
createProject,
handleProjectCreate,
updateProject,
handleProjectUpdate,
updateProjectBackgroundImage,
deleteProject,
handleProjectDelete,
};
+43
View File
@@ -0,0 +1,43 @@
import ActionTypes from '../constants/ActionTypes';
const handleLocationChange = (
board,
users,
projects,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
deletedNotifications,
) => ({
type: ActionTypes.LOCATION_CHANGE_HANDLE,
payload: {
board,
users,
projects,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
deletedNotifications,
},
});
handleLocationChange.fetchBoard = (id) => ({
type: ActionTypes.LOCATION_CHANGE_HANDLE__BOARD_FETCH,
payload: {
id,
},
});
export default {
handleLocationChange,
};
+58
View File
@@ -0,0 +1,58 @@
import ActionTypes from '../constants/ActionTypes';
const handleSocketDisconnect = () => ({
type: ActionTypes.SOCKET_DISCONNECT_HANDLE,
payload: {},
});
const handleSocketReconnect = (
user,
board,
users,
projects,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
activities,
notifications,
) => ({
type: ActionTypes.SOCKET_RECONNECT_HANDLE,
payload: {
user,
board,
users,
projects,
projectManagers,
boards,
boardMemberships,
labels,
lists,
cards,
cardMemberships,
cardLabels,
tasks,
attachments,
activities,
notifications,
},
});
handleSocketReconnect.fetchCore = (currentUserId, currentBoardId) => ({
type: ActionTypes.SOCKET_RECONNECT_HANDLE__CORE_FETCH,
payload: {
currentUserId,
currentBoardId,
},
});
export default {
handleSocketDisconnect,
handleSocketReconnect,
};
+99
View File
@@ -0,0 +1,99 @@
import ActionTypes from '../constants/ActionTypes';
const createTask = (task) => ({
type: ActionTypes.TASK_CREATE,
payload: {
task,
},
});
createTask.success = (localId, task) => ({
type: ActionTypes.TASK_CREATE__SUCCESS,
payload: {
localId,
task,
},
});
createTask.failure = (localId, error) => ({
type: ActionTypes.TASK_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleTaskCreate = (task) => ({
type: ActionTypes.TASK_CREATE_HANDLE,
payload: {
task,
},
});
const updateTask = (id, data) => ({
type: ActionTypes.TASK_UPDATE,
payload: {
id,
data,
},
});
updateTask.success = (task) => ({
type: ActionTypes.TASK_UPDATE__SUCCESS,
payload: {
task,
},
});
updateTask.failure = (id, error) => ({
type: ActionTypes.TASK_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleTaskUpdate = (task) => ({
type: ActionTypes.TASK_UPDATE_HANDLE,
payload: {
task,
},
});
const deleteTask = (id) => ({
type: ActionTypes.TASK_DELETE,
payload: {
id,
},
});
deleteTask.success = (task) => ({
type: ActionTypes.TASK_DELETE__SUCCESS,
payload: {
task,
},
});
deleteTask.failure = (id, error) => ({
type: ActionTypes.TASK_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleTaskDelete = (task) => ({
type: ActionTypes.TASK_DELETE_HANDLE,
payload: {
task,
},
});
export default {
createTask,
handleTaskCreate,
updateTask,
handleTaskUpdate,
deleteTask,
handleTaskDelete,
};
+309
View File
@@ -0,0 +1,309 @@
import ActionTypes from '../constants/ActionTypes';
const createUser = (data) => ({
type: ActionTypes.USER_CREATE,
payload: {
data,
},
});
createUser.success = (user) => ({
type: ActionTypes.USER_CREATE__SUCCESS,
payload: {
user,
},
});
createUser.failure = (error) => ({
type: ActionTypes.USER_CREATE__FAILURE,
payload: {
error,
},
});
const handleUserCreate = (user) => ({
type: ActionTypes.USER_CREATE_HANDLE,
payload: {
user,
},
});
const clearUserCreateError = () => ({
type: ActionTypes.USER_CREATE_ERROR_CLEAR,
payload: {},
});
const updateUser = (id, data) => ({
type: ActionTypes.USER_UPDATE,
payload: {
id,
data,
},
});
updateUser.success = (user) => ({
type: ActionTypes.USER_UPDATE__SUCCESS,
payload: {
user,
},
});
updateUser.failure = (id, error) => ({
type: ActionTypes.USER_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleUserUpdate = (user, users, isCurrent) => ({
type: ActionTypes.USER_UPDATE_HANDLE,
payload: {
user,
users,
isCurrent,
},
});
const updateUserEmail = (id, data) => ({
type: ActionTypes.USER_EMAIL_UPDATE,
payload: {
id,
data,
},
});
updateUserEmail.success = (user) => ({
type: ActionTypes.USER_EMAIL_UPDATE__SUCCESS,
payload: {
user,
},
});
updateUserEmail.failure = (id, error) => ({
type: ActionTypes.USER_EMAIL_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const clearUserEmailUpdateError = (id) => ({
type: ActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
const updateUserPassword = (id, data) => ({
type: ActionTypes.USER_PASSWORD_UPDATE,
payload: {
id,
data,
},
});
updateUserPassword.success = (user) => ({
type: ActionTypes.USER_PASSWORD_UPDATE__SUCCESS,
payload: {
user,
},
});
updateUserPassword.failure = (id, error) => ({
type: ActionTypes.USER_PASSWORD_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const clearUserPasswordUpdateError = (id) => ({
type: ActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
const updateUserUsername = (id, data) => ({
type: ActionTypes.USER_USERNAME_UPDATE,
payload: {
id,
data,
},
});
updateUserUsername.success = (user) => ({
type: ActionTypes.USER_USERNAME_UPDATE__SUCCESS,
payload: {
user,
},
});
updateUserUsername.failure = (id, error) => ({
type: ActionTypes.USER_USERNAME_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const clearUserUsernameUpdateError = (id) => ({
type: ActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
const updateUserAvatar = (id) => ({
type: ActionTypes.USER_AVATAR_UPDATE,
payload: {
id,
},
});
updateUserAvatar.success = (user) => ({
type: ActionTypes.USER_AVATAR_UPDATE__SUCCESS,
payload: {
user,
},
});
updateUserAvatar.failure = (id, error) => ({
type: ActionTypes.USER_AVATAR_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const deleteUser = (id) => ({
type: ActionTypes.USER_DELETE,
payload: {
id,
},
});
deleteUser.success = (user) => ({
type: ActionTypes.USER_DELETE__SUCCESS,
payload: {
user,
},
});
deleteUser.failure = (id, error) => ({
type: ActionTypes.USER_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleUserDelete = (user) => ({
type: ActionTypes.USER_DELETE_HANDLE,
payload: {
user,
},
});
const addUserToCard = (id, cardId, isCurrent) => ({
type: ActionTypes.USER_TO_CARD_ADD,
payload: {
id,
cardId,
isCurrent,
},
});
addUserToCard.success = (cardMembership) => ({
type: ActionTypes.USER_TO_CARD_ADD__SUCCESS,
payload: {
cardMembership,
},
});
addUserToCard.failure = (id, cardId, error) => ({
type: ActionTypes.USER_TO_CARD_ADD__FAILURE,
payload: {
id,
cardId,
error,
},
});
const handleUserToCardAdd = (cardMembership) => ({
type: ActionTypes.USER_TO_CARD_ADD_HANDLE,
payload: {
cardMembership,
},
});
const removeUserFromCard = (id, cardId) => ({
type: ActionTypes.USER_FROM_CARD_REMOVE,
payload: {
id,
cardId,
},
});
removeUserFromCard.success = (cardMembership) => ({
type: ActionTypes.USER_FROM_CARD_REMOVE__SUCCESS,
payload: {
cardMembership,
},
});
removeUserFromCard.failure = (id, cardId, error) => ({
type: ActionTypes.USER_FROM_CARD_REMOVE__FAILURE,
payload: {
id,
cardId,
error,
},
});
const handleUserFromCardRemove = (cardMembership) => ({
type: ActionTypes.USER_FROM_CARD_REMOVE_HANDLE,
payload: {
cardMembership,
},
});
const addUserToBoardFilter = (id, boardId) => ({
type: ActionTypes.USER_TO_BOARD_FILTER_ADD,
payload: {
id,
boardId,
},
});
const removeUserFromBoardFilter = (id, boardId) => ({
type: ActionTypes.USER_FROM_BOARD_FILTER_REMOVE,
payload: {
id,
boardId,
},
});
export default {
createUser,
handleUserCreate,
clearUserCreateError,
updateUser,
handleUserUpdate,
updateUserEmail,
clearUserEmailUpdateError,
updateUserPassword,
clearUserPasswordUpdateError,
updateUserUsername,
clearUserUsernameUpdateError,
updateUserAvatar,
deleteUser,
handleUserDelete,
addUserToCard,
handleUserToCardAdd,
removeUserFromCard,
handleUserFromCardRemove,
addUserToBoardFilter,
removeUserFromBoardFilter,
};
+9
View File
@@ -0,0 +1,9 @@
import http from './http';
/* Actions */
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
export default {
createAccessToken,
};
+36
View File
@@ -0,0 +1,36 @@
import socket from './socket';
/* Transformers */
export const transformActivity = (activity) => ({
...activity,
createdAt: new Date(activity.createdAt),
});
/* Actions */
const getActivities = (cardId, data, headers) =>
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
...body,
items: body.items.map(transformActivity),
}));
/* Event handlers */
const makeHandleActivityCreate = (next) => (body) => {
next({
...body,
item: transformActivity(body.item),
});
};
const makeHandleActivityUpdate = makeHandleActivityCreate;
const makeHandleActivityDelete = makeHandleActivityCreate;
export default {
getActivities,
makeHandleActivityCreate,
makeHandleActivityUpdate,
makeHandleActivityDelete,
};
+51
View File
@@ -0,0 +1,51 @@
import http from './http';
import socket from './socket';
/* Transformers */
export const transformAttachment = (attachment) => ({
...attachment,
createdAt: new Date(attachment.createdAt),
});
/* Actions */
const createAttachment = (cardId, data, requestId, headers) =>
http.post(`/cards/${cardId}/attachments?requestId=${requestId}`, data, headers).then((body) => ({
...body,
item: transformAttachment(body.item),
}));
const updateAttachment = (id, data, headers) =>
socket.patch(`/attachments/${id}`, data, headers).then((body) => ({
...body,
item: transformAttachment(body.item),
}));
const deleteAttachment = (id, headers) =>
socket.delete(`/attachments/${id}`, undefined, headers).then((body) => ({
...body,
item: transformAttachment(body.item),
}));
/* Event handlers */
const makeHandleAttachmentCreate = (next) => (body) => {
next({
...body,
item: transformAttachment(body.item),
});
};
const makeHandleAttachmentUpdate = makeHandleAttachmentCreate;
const makeHandleAttachmentDelete = makeHandleAttachmentCreate;
export default {
createAttachment,
updateAttachment,
deleteAttachment,
makeHandleAttachmentCreate,
makeHandleAttachmentUpdate,
makeHandleAttachmentDelete,
};
+18
View File
@@ -0,0 +1,18 @@
import socket from './socket';
/* Actions */
const createBoardMembership = (boardId, data, headers) =>
socket.post(`/boards/${boardId}/memberships`, data, headers);
const updateBoardMembership = (id, data, headers) =>
socket.patch(`/board-memberships/${id}`, data, headers);
const deleteBoardMembership = (id, headers) =>
socket.delete(`/board-memberships/${id}`, undefined, headers);
export default {
createBoardMembership,
updateBoardMembership,
deleteBoardMembership,
};
+29
View File
@@ -0,0 +1,29 @@
import socket from './socket';
import { transformCard } from './cards';
import { transformAttachment } from './attachments';
/* Actions */
const createBoard = (projectId, data, headers) =>
socket.post(`/projects/${projectId}/boards`, data, headers);
const getBoard = (id, headers) =>
socket.get(`/boards/${id}`, undefined, headers).then((body) => ({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
attachments: body.included.attachments.map(transformAttachment),
},
}));
const updateBoard = (id, data, headers) => socket.patch(`/boards/${id}`, data, headers);
const deleteBoard = (id, headers) => socket.delete(`/boards/${id}`, undefined, headers);
export default {
createBoard,
getBoard,
updateBoard,
deleteBoard,
};
+14
View File
@@ -0,0 +1,14 @@
import socket from './socket';
/* Actions */
const createCardLabel = (cardId, data, headers) =>
socket.post(`/cards/${cardId}/labels`, data, headers);
const deleteCardLabel = (cardId, labelId, headers) =>
socket.delete(`/cards/${cardId}/labels/${labelId}`, undefined, headers);
export default {
createCardLabel,
deleteCardLabel,
};
+14
View File
@@ -0,0 +1,14 @@
import socket from './socket';
/* Actions */
const createCardMembership = (cardId, data, headers) =>
socket.post(`/cards/${cardId}/memberships`, data, headers);
const deleteCardMembership = (cardId, userId, headers) =>
socket.delete(`/cards/${cardId}/memberships?userId=${userId}`, undefined, headers);
export default {
createCardMembership,
deleteCardMembership,
};
+94
View File
@@ -0,0 +1,94 @@
import socket from './socket';
import { transformAttachment } from './attachments';
/* Transformers */
export const transformCard = (card) => ({
...card,
...(card.dueDate && {
dueDate: new Date(card.dueDate),
}),
...(card.timer && {
timer: {
...card.timer,
...(card.timer.startedAt && {
startedAt: new Date(card.timer.startedAt),
}),
},
}),
});
export const transformCardData = (data) => ({
...data,
...(data.dueDate && {
dueDate: data.dueDate.toISOString(),
}),
...(data.timer && {
timer: {
...data.timer,
...(data.timer.startedAt && {
startedAt: data.timer.startedAt.toISOString(),
}),
},
}),
});
/* Actions */
const getCards = (boardId, data, headers) =>
socket.get(`/board/${boardId}/cards`, data, headers).then((body) => ({
...body,
items: body.items.map(transformCard),
included: {
...body.included,
attachments: body.included.attachments.map(transformAttachment),
},
}));
const createCard = (boardId, data, headers) =>
socket.post(`/boards/${boardId}/cards`, transformCardData(data), headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const getCard = (id, headers) =>
socket.get(`/cards/${id}`, undefined, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const updateCard = (id, data, headers) =>
socket.patch(`/cards/${id}`, transformCardData(data), headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const deleteCard = (id, headers) =>
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
/* Event handlers */
const makeHandleCardCreate = (next) => (body) => {
next({
...body,
item: transformCard(body.item),
});
};
const makeHandleCardUpdate = makeHandleCardCreate;
const makeHandleCardDelete = makeHandleCardCreate;
export default {
getCards,
createCard,
getCard,
updateCard,
deleteCard,
makeHandleCardCreate,
makeHandleCardUpdate,
makeHandleCardDelete,
};
+28
View File
@@ -0,0 +1,28 @@
import socket from './socket';
import { transformActivity } from './activities';
/* Actions */
const createCommentActivity = (cardId, data, headers) =>
socket.post(`/cards/${cardId}/comment-actions`, data, headers).then((body) => ({
...body,
item: transformActivity(body.item),
}));
const updateCommentActivity = (id, data, headers) =>
socket.patch(`/comment-actions/${id}`, data, headers).then((body) => ({
...body,
item: transformActivity(body.item),
}));
const deleteCommentActivity = (id, headers) =>
socket.delete(`/comment-actions/${id}`, undefined, headers).then((body) => ({
...body,
item: transformActivity(body.item),
}));
export default {
createCommentActivity,
updateCommentActivity,
deleteCommentActivity,
};
+37
View File
@@ -0,0 +1,37 @@
import { fetch } from 'whatwg-fetch';
import Config from '../constants/Config';
const http = {};
// TODO: add all methods
['POST'].forEach((method) => {
http[method.toLowerCase()] = (url, data, headers) => {
const formData = Object.keys(data).reduce((result, key) => {
result.append(key, data[key]);
return result;
}, new FormData());
return fetch(`${Config.SERVER_BASE_URL}/api${url}`, {
method,
headers,
body: formData,
})
.then((response) =>
response.json().then((body) => ({
body,
isError: response.status !== 200,
})),
)
.then(({ body, isError }) => {
if (isError) {
throw body;
}
return body;
});
};
});
export default http;
+39
View File
@@ -0,0 +1,39 @@
import http from './http';
import socket from './socket';
import accessTokens from './access-tokens';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
import boards from './boards';
import boardMemberships from './board-memberships';
import labels from './labels';
import lists from './lists';
import cards from './cards';
import cardMemberships from './card-memberships';
import cardLabels from './card-labels';
import tasks from './tasks';
import attachments from './attachments';
import activities from './activities';
import commentActivities from './comment-activities';
import notifications from './notifications';
export { http, socket };
export default {
...accessTokens,
...users,
...projects,
...projectManagers,
...boards,
...boardMemberships,
...labels,
...lists,
...cards,
...cardMemberships,
...cardLabels,
...tasks,
...attachments,
...activities,
...commentActivities,
...notifications,
};
+16
View File
@@ -0,0 +1,16 @@
import socket from './socket';
/* Actions */
const createLabel = (boardId, data, headers) =>
socket.post(`/boards/${boardId}/labels`, data, headers);
const updateLabel = (id, data, headers) => socket.patch(`/labels/${id}`, data, headers);
const deleteLabel = (id, headers) => socket.delete(`/labels/${id}`, undefined, headers);
export default {
createLabel,
updateLabel,
deleteLabel,
};
+16
View File
@@ -0,0 +1,16 @@
import socket from './socket';
/* Actions */
const createList = (boardId, data, headers) =>
socket.post(`/boards/${boardId}/lists`, data, headers);
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
export default {
createList,
updateList,
deleteList,
};
+61
View File
@@ -0,0 +1,61 @@
import omit from 'lodash/omit';
import socket from './socket';
import { transformCard } from './cards';
import { transformActivity } from './activities';
/* Transformers */
export const transformNotification = (notification) => ({
...omit(notification, 'actionId'),
activityId: notification.actionId,
});
/* Actions */
const getNotifications = (headers) =>
socket.get('/notifications', undefined, headers).then((body) => ({
...body,
items: body.items.map(transformNotification),
included: {
...omit(body.included, 'actions'),
cards: body.included.cards.map(transformCard),
activities: body.included.actions.map(transformActivity),
},
}));
const getNotification = (id, headers) =>
socket.get(`/notifications/${id}`, undefined, headers).then((body) => ({
...body,
item: transformNotification(body.item),
included: {
...omit(body.included, 'actions'),
cards: body.included.cards.map(transformCard),
activities: body.included.actions.map(transformActivity),
},
}));
const updateNotifications = (ids, data, headers) =>
socket.patch(`/notifications/${ids.join(',')}`, data, headers).then((body) => ({
...body,
items: body.items.map(transformNotification),
}));
/* Event handlers */
const makeHandleNotificationCreate = (next) => (body) => {
next({
...body,
item: transformNotification(body.item),
});
};
const makeHandleNotificationUpdate = makeHandleNotificationCreate;
export default {
getNotifications,
getNotification,
updateNotifications,
makeHandleNotificationCreate,
makeHandleNotificationUpdate,
};
+14
View File
@@ -0,0 +1,14 @@
import socket from './socket';
/* Actions */
const createProjectManager = (projectId, data, headers) =>
socket.post(`/projects/${projectId}/managers`, data, headers);
const deleteProjectManager = (id, headers) =>
socket.delete(`/project-managers/${id}`, undefined, headers);
export default {
createProjectManager,
deleteProjectManager,
};
+26
View File
@@ -0,0 +1,26 @@
import http from './http';
import socket from './socket';
/* Actions */
const getProjects = (headers) => socket.get('/projects', undefined, headers);
const createProject = (data, headers) => socket.post('/projects', data, headers);
const getProject = (id, headers) => socket.get(`/projects/${id}`, undefined, headers);
const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers);
const updateProjectBackgroundImage = (id, data, headers) =>
http.post(`/projects/${id}/background-image`, data, headers);
const deleteProject = (id, headers) => socket.delete(`/projects/${id}`, undefined, headers);
export default {
getProjects,
createProject,
getProject,
updateProject,
updateProjectBackgroundImage,
deleteProject,
};
+39
View File
@@ -0,0 +1,39 @@
import socketIOClient from 'socket.io-client';
import sailsIOClient from 'sails.io.js';
import Config from '../constants/Config';
const io = sailsIOClient(socketIOClient);
io.sails.url = Config.SERVER_BASE_URL;
io.sails.autoConnect = false;
io.sails.reconnection = true;
io.sails.useCORSRouteToGetCookie = false;
io.sails.environment = process.env.NODE_ENV;
const { socket } = io;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
socket[method.toLowerCase()] = (url, data, headers) =>
new Promise((resolve, reject) => {
socket.request(
{
method,
data,
headers,
url: `/api${url}`,
},
(_, { body, error }) => {
if (error) {
reject(body);
} else {
resolve(body);
}
},
);
});
});
export default socket;
+15
View File
@@ -0,0 +1,15 @@
import socket from './socket';
/* Actions */
const createTask = (cardId, data, headers) => socket.post(`/cards/${cardId}/tasks`, data, headers);
const updateTask = (id, data, headers) => socket.patch(`/tasks/${id}`, data, headers);
const deleteTask = (id, headers) => socket.delete(`/tasks/${id}`, undefined, headers);
export default {
createTask,
updateTask,
deleteTask,
};
+39
View File
@@ -0,0 +1,39 @@
import http from './http';
import socket from './socket';
/* Actions */
const getUsers = (headers) => socket.get('/users', undefined, headers);
const createUser = (data, headers) => socket.post('/users', data, headers);
const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers);
const getCurrentUser = (headers) => socket.get('/users/me', undefined, headers);
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers);
const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers);
const updateUserPassword = (id, data, headers) =>
socket.patch(`/users/${id}/password`, data, headers);
const updateUserUsername = (id, data, headers) =>
socket.patch(`/users/${id}/username`, data, headers);
const updateUserAvatar = (id, data, headers) => http.post(`/users/${id}/avatar`, data, headers);
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers);
export default {
getUsers,
createUser,
getUser,
getCurrentUser,
updateUser,
updateUserEmail,
updateUserPassword,
updateUserUsername,
updateUserAvatar,
deleteUser,
};

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 559 KiB

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

Before

Width:  |  Height:  |  Size: 142 B

After

Width:  |  Height:  |  Size: 142 B

@@ -0,0 +1,38 @@
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { ProjectBackgroundTypes } from '../../constants/Enums';
import styles from './Background.module.scss';
import globalStyles from '../../styles.module.scss';
function Background({ type, name, imageUrl }) {
return (
<div
className={classNames(
styles.wrapper,
type === ProjectBackgroundTypes.GRADIENT &&
globalStyles[`background${upperFirst(camelCase(name))}`],
)}
style={{
background: type === 'image' && `url("${imageUrl}") center / cover`,
}}
/>
);
}
Background.propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string,
imageUrl: PropTypes.string,
};
Background.defaultProps = {
name: undefined,
imageUrl: undefined,
};
export default Background;
@@ -0,0 +1,8 @@
:global(#app) {
.wrapper {
height: 100%;
position: fixed;
width: 100%;
z-index: -1;
}
}
@@ -0,0 +1,3 @@
import Background from './Background';
export default Background;
@@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import Filters from './Filters';
import Memberships from '../Memberships';
import BoardMembershipPermissionsSelectStep from '../BoardMembershipPermissionsSelectStep';
import styles from './BoardActions.module.scss';
const BoardActions = React.memo(
({
memberships,
labels,
filterUsers,
filterLabels,
allUsers,
canEditMemberships,
onMembershipCreate,
onMembershipUpdate,
onMembershipDelete,
onUserToFilterAdd,
onUserFromFilterRemove,
onLabelToFilterAdd,
onLabelFromFilterRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
}) => {
return (
<div className={styles.actions}>
<div className={styles.action}>
<Memberships
items={memberships}
allUsers={allUsers}
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
canEdit={canEditMemberships}
onCreate={onMembershipCreate}
onUpdate={onMembershipUpdate}
onDelete={onMembershipDelete}
/>
</div>
<div className={styles.action}>
<Filters
users={filterUsers}
labels={filterLabels}
allBoardMemberships={memberships}
allLabels={labels}
onUserAdd={onUserToFilterAdd}
onUserRemove={onUserFromFilterRemove}
onLabelAdd={onLabelToFilterAdd}
onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelDelete={onLabelDelete}
/>
</div>
</div>
);
},
);
BoardActions.propTypes = {
/* eslint-disable react/forbid-prop-types */
memberships: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEditMemberships: PropTypes.bool.isRequired,
onMembershipCreate: PropTypes.func.isRequired,
onMembershipUpdate: PropTypes.func.isRequired,
onMembershipDelete: PropTypes.func.isRequired,
onUserToFilterAdd: PropTypes.func.isRequired,
onUserFromFilterRemove: PropTypes.func.isRequired,
onLabelToFilterAdd: PropTypes.func.isRequired,
onLabelFromFilterRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};
export default BoardActions;
@@ -0,0 +1,11 @@
:global(#app) {
.action {
margin-right: 20px;
}
.actions {
align-items: center;
display: flex;
margin: 20px 20px;
}
}
@@ -0,0 +1,120 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import User from '../User';
import Label from '../Label';
import BoardMembershipsPopup from '../BoardMembershipsPopup';
import LabelsPopup from '../LabelsPopup';
import styles from './Filters.module.scss';
const Filters = React.memo(
({
users,
labels,
allBoardMemberships,
allLabels,
onUserAdd,
onUserRemove,
onLabelAdd,
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
}) => {
const [t] = useTranslation();
const handleRemoveUserClick = useCallback(
(id) => {
onUserRemove(id);
},
[onUserRemove],
);
const handleRemoveLabelClick = useCallback(
(id) => {
onLabelRemove(id);
},
[onLabelRemove],
);
return (
<>
<span className={styles.filter}>
<BoardMembershipsPopup
items={allBoardMemberships}
currentUserIds={users.map((user) => user.id)}
title={t('common.filterByMembers', {
context: 'title',
})}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
<button type="button" className={styles.filterButton}>
<span className={styles.filterTitle}>{`${t('common.members')}:`}</span>
{users.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
</button>
</BoardMembershipsPopup>
{users.map((user) => (
<span key={user.id} className={styles.filterItem}>
<User
name={user.name}
avatarUrl={user.avatarUrl}
size="tiny"
onClick={() => handleRemoveUserClick(user.id)}
/>
</span>
))}
</span>
<span className={styles.filter}>
<LabelsPopup
items={allLabels}
currentIds={labels.map((label) => label.id)}
title={t('common.filterByLabels', {
context: 'title',
})}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
>
<button type="button" className={styles.filterButton}>
<span className={styles.filterTitle}>{`${t('common.labels')}:`}</span>
{labels.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
</button>
</LabelsPopup>
{labels.map((label) => (
<span key={label.id} className={styles.filterItem}>
<Label
name={label.name}
color={label.color}
size="small"
onClick={() => handleRemoveLabelClick(label.id)}
/>
</span>
))}
</span>
</>
);
},
);
Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};
export default Filters;
@@ -0,0 +1,46 @@
:global(#app) {
.filter {
margin-right: 10px;
}
.filterButton {
background: transparent;
border: none;
cursor: pointer;
display: inline-block;
outline: none;
padding: 0;
}
.filterItem {
display: inline-block;
font-size: 0;
line-height: 0;
margin-right: 4px;
max-width: 190px;
vertical-align: top;
}
.filterLabel {
background: rgba(0, 0, 0, 0.24);
border-radius: 3px;
color: #fff;
display: inline-block;
font-size: 12px;
line-height: 20px;
padding: 2px 8px;
&:hover {
background: rgba(0, 0, 0, 0.32);
}
}
.filterTitle {
border-radius: 3px;
color: #fff;
display: inline-block;
font-size: 12px;
line-height: 20px;
padding: 2px 12px;
}
}
@@ -0,0 +1,3 @@
import BoardActions from './BoardActions';
export default BoardActions;
+178
View File
@@ -0,0 +1,178 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { closePopup } from '../../lib/popup';
import DroppableTypes from '../../constants/DroppableTypes';
import ListContainer from '../../containers/ListContainer';
import CardModalContainer from '../../containers/CardModalContainer';
import ListAdd from './ListAdd';
import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-icon.svg';
import styles from './BoardKanban.module.scss';
const parseDndId = (dndId) => dndId.split(':')[1];
const BoardKanban = React.memo(
({ listIds, isCardModalOpened, canEdit, onListCreate, onListMove, onCardMove }) => {
const [t] = useTranslation();
const [isListAddOpened, setIsListAddOpened] = useState(false);
const wrapper = useRef(null);
const prevPosition = useRef(null);
const handleAddListClick = useCallback(() => {
setIsListAddOpened(true);
}, []);
const handleAddListClose = useCallback(() => {
setIsListAddOpened(false);
}, []);
const handleDragStart = useCallback(() => {
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, type, source, destination }) => {
if (
!destination ||
(source.droppableId === destination.droppableId && source.index === destination.index)
) {
return;
}
const id = parseDndId(draggableId);
switch (type) {
case DroppableTypes.LIST:
onListMove(id, destination.index);
break;
case DroppableTypes.CARD:
onCardMove(id, parseDndId(destination.droppableId), destination.index);
break;
default:
}
},
[onListMove, onCardMove],
);
const handleMouseDown = useCallback(
(event) => {
if (event.target !== wrapper.current && !event.target.dataset.dragScroller) {
return;
}
prevPosition.current = event.clientX;
},
[wrapper],
);
const handleWindowMouseMove = useCallback(
(event) => {
if (!prevPosition.current) {
return;
}
event.preventDefault();
window.scrollBy({
left: prevPosition.current - event.clientX,
});
prevPosition.current = event.clientX;
},
[prevPosition],
);
const handleWindowMouseUp = useCallback(() => {
prevPosition.current = null;
}, [prevPosition]);
useEffect(() => {
document.body.style.overflowX = 'auto';
return () => {
document.body.style.overflowX = null;
};
}, []);
useEffect(() => {
if (isListAddOpened) {
window.scroll(document.body.scrollWidth, 0);
}
}, [listIds, isListAddOpened]);
useEffect(() => {
window.addEventListener('mouseup', handleWindowMouseUp);
window.addEventListener('mousemove', handleWindowMouseMove);
return () => {
window.removeEventListener('mouseup', handleWindowMouseUp);
window.removeEventListener('mousemove', handleWindowMouseMove);
};
}, [handleWindowMouseUp, handleWindowMouseMove]);
return (
<>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div ref={wrapper} className={styles.wrapper} onMouseDown={handleMouseDown}>
<div>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="board" type={DroppableTypes.LIST} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => (
<div
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
data-drag-scroller
ref={innerRef}
className={styles.lists}
>
{listIds.map((listId, index) => (
<ListContainer key={listId} id={listId} index={index} />
))}
{placeholder}
{canEdit && (
<div data-drag-scroller className={styles.list}>
{isListAddOpened ? (
<ListAdd onCreate={onListCreate} onClose={handleAddListClose} />
) : (
<button
type="button"
className={styles.addListButton}
onClick={handleAddListClick}
>
<PlusMathIcon className={styles.addListButtonIcon} />
<span className={styles.addListButtonText}>
{listIds.length > 0
? t('action.addAnotherList')
: t('action.addList')}
</span>
</button>
)}
</div>
)}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
{isCardModalOpened && <CardModalContainer />}
</>
);
},
);
BoardKanban.propTypes = {
listIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isCardModalOpened: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onListCreate: PropTypes.func.isRequired,
onListMove: PropTypes.func.isRequired,
onCardMove: PropTypes.func.isRequired,
};
export default BoardKanban;
@@ -0,0 +1,64 @@
:global(#app) {
.addListButton {
background: rgba(0, 0, 0, 0.24);
border: none;
border-radius: 3px;
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
display: block;
fill: rgba(255, 255, 255, 0.72);
font-weight: normal;
height: 42px;
padding: 11px;
text-align: left;
transition: background 85ms ease-in, opacity 40ms ease-in,
border-color 85ms ease-in;
width: 100%;
&:active {
outline: none;
}
&:hover {
background: rgba(0, 0, 0, 0.32);
}
}
.addListButtonIcon {
height: 20px;
padding: 0.64px;
width: 20px;
}
.addListButtonText {
display: inline-block;
font-size: 14px;
line-height: 20px;
vertical-align: top;
}
.list {
margin-right: 20px;
width: 272px;
}
.lists {
display: inline-flex;
height: 100%;
min-width: 100%;
}
.panel {
align-items: center;
display: flex;
margin-bottom: 20px;
}
.panelItem {
margin-right: 20px;
}
.wrapper {
margin: 0 20px;
}
}
+89
View File
@@ -0,0 +1,89 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
import { useDidUpdate, useToggle } from '../../lib/hooks';
import { useClosableForm, useForm } from '../../hooks';
import styles from './ListAdd.module.scss';
const DEFAULT_DATA = {
name: '',
};
const ListAdd = React.memo(({ onCreate, onClose }) => {
const [t] = useTranslation();
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [focusNameFieldState, focusNameField] = useToggle();
const nameField = useRef(null);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.key === 'Escape') {
onClose();
}
},
[onClose],
);
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(onClose);
const handleSubmit = useCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
focusNameField();
}, [onCreate, data, setData, focusNameField]);
useEffect(() => {
nameField.current.focus();
}, []);
useDidUpdate(() => {
nameField.current.focus();
}, [focusNameFieldState]);
return (
<Form className={styles.wrapper} onSubmit={handleSubmit}>
<Input
ref={nameField}
name="name"
value={data.name}
placeholder={t('common.enterListTitle')}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
/>
<div className={styles.controls}>
{/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */}
<Button
positive
content={t('action.addList')}
className={styles.submitButton}
onMouseOver={handleControlMouseOver}
onMouseOut={handleControlMouseOut}
/>
</div>
</Form>
);
});
ListAdd.propTypes = {
onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default ListAdd;
@@ -0,0 +1,33 @@
:global(#app) {
.controls {
margin-top: 4px;
}
.field {
border: none;
border-radius: 3px;
box-shadow: 0 1px 0 #ccc;
color: #333;
outline: none;
overflow: hidden;
width: 100%;
&:focus {
border-color: #298fca;
box-shadow: 0 0 2px #298fca;
}
}
.submitButton {
min-height: 30px;
vertical-align: top;
}
.wrapper {
background: #e2e4e6;
border-radius: 3px;
padding: 4px;
transition: opacity 40ms ease-in;
width: 272px;
}
}
+3
View File
@@ -0,0 +1,3 @@
import BoardKanban from './BoardKanban';
export default BoardKanban;
@@ -0,0 +1,101 @@
import omit from 'lodash/omit';
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Menu, Radio, Segment } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { BoardMembershipRoles } from '../../constants/Enums';
import styles from './BoardMembershipPermissionsSelectStep.module.scss';
const BoardMembershipPermissionsSelectStep = React.memo(
({ defaultData, title, buttonContent, onSelect, onBack }) => {
const [t] = useTranslation();
const [data, setData] = useState(() => ({
role: BoardMembershipRoles.EDITOR,
canComment: null,
...defaultData,
}));
const handleRoleSelectClick = useCallback((role) => {
setData((prevData) => ({
...prevData,
role,
canComment: role === BoardMembershipRoles.EDITOR ? null : !!prevData.canComment,
}));
}, []);
const handleSettingChange = useCallback((_, { name: fieldName, checked: value }) => {
setData((prevData) => ({
...prevData,
[fieldName]: value,
}));
}, []);
const handleSubmit = useCallback(() => {
onSelect(data.role === BoardMembershipRoles.EDITOR ? omit(data, 'canComment') : data);
}, [onSelect, data]);
return (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Menu secondary vertical className={styles.menu}>
<Menu.Item
active={data.role === BoardMembershipRoles.EDITOR}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)}
>
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
<div className={styles.menuItemDescription}>
{t('common.canEditContentOfBoard')}
</div>
</Menu.Item>
<Menu.Item
active={data.role === BoardMembershipRoles.VIEWER}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)}
>
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
</Menu.Item>
</Menu>
{data.role !== BoardMembershipRoles.EDITOR && (
<Segment basic className={styles.settings}>
<Radio
toggle
name="canComment"
checked={data.canComment}
label={t('common.canComment')}
onChange={handleSettingChange}
/>
</Segment>
)}
<Button positive content={t(buttonContent)} />
</Form>
</Popup.Content>
</>
);
},
);
BoardMembershipPermissionsSelectStep.propTypes = {
defaultData: PropTypes.object, // eslint-disable-line react/forbid-prop-types
title: PropTypes.string,
buttonContent: PropTypes.string,
onSelect: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
BoardMembershipPermissionsSelectStep.defaultProps = {
defaultData: undefined,
title: 'common.selectPermissions',
buttonContent: 'action.selectPermissions',
};
export default BoardMembershipPermissionsSelectStep;
@@ -0,0 +1,18 @@
:global(#app) {
.menu {
margin: 0 auto 8px;
width: 100%;
}
.menuItemDescription {
opacity: 0.5;
}
.menuItemTitle {
margin-bottom: 8px;
}
.settings {
margin: 0 0 8px;
}
}
@@ -0,0 +1,3 @@
import BoardMembershipPermissionsSelectStep from './BoardMembershipPermissionsSelectStep';
export default BoardMembershipPermissionsSelectStep;
@@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import BoardMembershipsStep from './BoardMembershipsStep';
export default withPopup(BoardMembershipsStep);
@@ -0,0 +1,101 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { Input, Popup } from '../../lib/custom-ui';
import { useField } from '../../hooks';
import Item from './Item';
import styles from './BoardMembershipsStep.module.scss';
const BoardMembershipsStep = React.memo(
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
const [t] = useTranslation();
const [search, handleSearchChange] = useField('');
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
const filteredItems = useMemo(
() =>
items.filter(
({ user }) =>
user.email.includes(cleanSearch) ||
user.name.toLowerCase().includes(cleanSearch) ||
(user.username && user.username.includes(cleanSearch)),
),
[items, cleanSearch],
);
const searchField = useRef(null);
const handleUserSelect = useCallback(
(id) => {
onUserSelect(id);
},
[onUserSelect],
);
const handleUserDeselect = useCallback(
(id) => {
onUserDeselect(id);
},
[onUserDeselect],
);
useEffect(() => {
searchField.current.focus();
}, []);
return (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Input
fluid
ref={searchField}
value={search}
placeholder={t('common.searchMembers')}
icon="search"
onChange={handleSearchChange}
/>
{filteredItems.length > 0 && (
<Menu secondary vertical className={styles.menu}>
{filteredItems.map((item) => (
<Item
key={item.id}
isPersisted={item.isPersisted}
isActive={currentUserIds.includes(item.user.id)}
user={item.user}
onUserSelect={() => handleUserSelect(item.user.id)}
onUserDeselect={() => handleUserDeselect(item.user.id)}
/>
))}
</Menu>
)}
</Popup.Content>
</>
);
},
);
BoardMembershipsStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
items: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
title: PropTypes.string,
onUserSelect: PropTypes.func.isRequired,
onUserDeselect: PropTypes.func.isRequired,
onBack: PropTypes.func,
};
BoardMembershipsStep.defaultProps = {
title: 'common.members',
onBack: undefined,
};
export default BoardMembershipsStep;
@@ -0,0 +1,22 @@
:global(#app) {
.menu {
margin: 8px auto 0;
max-height: 320px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
width: 100%;
&::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Menu } from 'semantic-ui-react';
import User from '../User';
import styles from './Item.module.scss';
const Item = React.memo(({ isPersisted, isActive, user, onUserSelect, onUserDeselect }) => {
const handleToggleClick = useCallback(() => {
if (isActive) {
onUserDeselect();
} else {
onUserSelect();
}
}, [isActive, onUserSelect, onUserDeselect]);
return (
<Menu.Item
active={isActive}
disabled={!isPersisted}
className={classNames(styles.menuItem, isActive && styles.menuItemActive)}
onClick={handleToggleClick}
>
<span className={styles.user}>
<User name={user.name} avatarUrl={user.avatarUrl} />
</span>
<div className={classNames(styles.menuItemText, isActive && styles.menuItemTextActive)}>
{user.name}
</div>
</Menu.Item>
);
});
Item.propTypes = {
isPersisted: PropTypes.bool.isRequired,
isActive: PropTypes.bool.isRequired,
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUserSelect: PropTypes.func.isRequired,
onUserDeselect: PropTypes.func.isRequired,
};
export default Item;
@@ -0,0 +1,43 @@
:global(#app) {
.menuItem {
display: block;
margin: 0;
overflow: hidden;
padding: 4px;
}
.menuItemActive {
background: transparent;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
}
.menuItemText {
display: inline-block;
line-height: 32px;
width: calc(100% - 40px);
}
.menuItemTextActive:before {
bottom: 2px;
color: #798d99;
content: "Г";
font-size: 18px;
font-weight: normal;
line-height: 36px;
position: absolute;
right: 2px;
text-align: center;
transform: rotate(-135deg);
width: 36px;
}
.user {
display: inline-block;
line-height: 32px;
padding-right: 8px;
width: 40px;
}
}
+3
View File
@@ -0,0 +1,3 @@
import BoardMembershipsStep from './BoardMembershipsStep';
export default BoardMembershipsStep;
+25
View File
@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loader } from 'semantic-ui-react';
import { BoardTypes } from '../constants/Enums';
import BoardKanbanContainer from '../containers/BoardKanbanContainer';
const BoardWrapper = React.memo(({ type, isFetching }) => {
if (isFetching) {
return <Loader active />;
}
if (type === BoardTypes.KANBAN) {
return <BoardKanbanContainer />;
}
return null;
});
BoardWrapper.propTypes = {
type: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
};
export default BoardWrapper;
+70
View File
@@ -0,0 +1,70 @@
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm } from '../../hooks';
import styles from './AddPopup.module.scss';
const AddStep = React.memo(({ onCreate, onClose }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm({
name: '',
});
const nameField = useRef(null);
const handleSubmit = useCallback(() => {
const cleanData = {
...data,
type: 'kanban',
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
onCreate(cleanData);
onClose();
}, [onCreate, onClose, data]);
useEffect(() => {
nameField.current.focus();
}, []);
return (
<>
<Popup.Header>
{t('common.createBoard', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.createBoard')} />
</Form>
</Popup.Content>
</>
);
});
AddStep.propTypes = {
onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(AddStep);
@@ -0,0 +1,5 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
}
+138
View File
@@ -0,0 +1,138 @@
import pick from 'lodash/pick';
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
import { closePopup } from '../../lib/popup';
import Paths from '../../constants/Paths';
import DroppableTypes from '../../constants/DroppableTypes';
import AddPopup from './AddPopup';
import EditPopup from './EditPopup';
import styles from './Boards.module.scss';
const Boards = React.memo(({ items, currentId, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
const tabsWrapper = useRef(null);
const handleWheel = useCallback(({ deltaY }) => {
tabsWrapper.current.scrollBy({
left: deltaY,
});
}, []);
const handleDragStart = useCallback(() => {
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
onMove(draggableId, destination.index);
},
[onMove],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const itemsNode = items.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}
isDragDisabled={!item.isPersisted || !canEdit}
>
{({ innerRef, draggableProps, dragHandleProps }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} ref={innerRef} className={styles.tabWrapper}>
<div className={classNames(styles.tab, item.id === currentId && styles.tabActive)}>
{item.isPersisted ? (
<>
<Link
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
to={Paths.BOARDS.replace(':id', item.id)}
title={item.name}
className={styles.link}
>
{item.name}
</Link>
{canEdit && (
<EditPopup
defaultData={pick(item, 'name')}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
>
<Button className={classNames(styles.editButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</EditPopup>
)}
</>
) : (
// eslint-disable-next-line react/jsx-props-no-spreading
<span {...dragHandleProps} className={styles.link}>
{item.name}
</span>
)}
</div>
</div>
)}
</Draggable>
));
return (
<div className={styles.wrapper} onWheel={handleWheel}>
<div ref={tabsWrapper} className={styles.tabsWrapper}>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="boards" type={DroppableTypes.BOARD} direction="horizontal">
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} ref={innerRef} className={styles.tabs}>
{itemsNode}
{placeholder}
{canEdit && (
<AddPopup onCreate={onCreate}>
<Button icon="plus" className={styles.addButton} />
</AddPopup>
)}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
);
});
Boards.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
currentId: PropTypes.string,
canEdit: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
Boards.defaultProps = {
currentId: undefined,
};
export default Boards;
@@ -0,0 +1,110 @@
:global(#app) {
.addButton {
background: transparent;
color: #fff;
margin-right: 0;
vertical-align: top;
&:hover {
background: rgba(34, 36, 38, 0.3);
}
}
.editButton {
background: transparent;
box-shadow: none;
color: #fff;
line-height: 32px;
margin-right: 0;
opacity: 0;
padding: 0;
position: absolute;
right: 2px;
top: 2px;
width: 32px;
&:hover {
background: rgba(255, 255, 255, 0.08);
}
}
.link {
color: #fff;
cursor: pointer;
display: block;
line-height: 20px;
padding: 10px 34px 6px 14px;
text-overflow: ellipsis;
max-width: 400px;
overflow: hidden;
}
.tab {
border-radius: 3px 3px 0 0;
min-width: 100px;
position: relative;
transition: all 0.1s ease;
&:hover {
background: rgba(0, 0, 0, 0.24);
.target {
opacity: 1;
}
}
}
.tabActive {
background: rgba(0, 0, 0, 0.24);
&:hover {
background: rgba(0, 0, 0, 0.32);
}
}
.tabWrapper {
display: flex;
flex: 0 0 auto;
}
.tabs {
display: flex;
height: 38px;
flex: 0 0 auto;
white-space: nowrap;
}
.tabsWrapper {
display: flex;
flex: 0 0 auto;
height: 56px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
&:hover {
height: 38px;
}
&::-webkit-scrollbar {
height: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
}
}
.wrapper {
border-bottom: 1px solid rgba(0, 0, 0, 0.24);
display: flex;
flex: 1 1 auto;
flex-direction: column;
height: 38px;
overflow: hidden;
}
}
+107
View File
@@ -0,0 +1,107 @@
import { dequal } from 'dequal';
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm, useSteps } from '../../hooks';
import DeleteStep from '../DeleteStep';
import styles from './EditPopup.module.scss';
const StepTypes = {
DELETE: 'DELETE',
};
const EditStep = React.memo(({ defaultData, onUpdate, onDelete, onClose }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
name: '',
...defaultData,
}));
const [step, openStep, handleBack] = useSteps();
const nameField = useRef(null);
const handleSubmit = useCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.select();
return;
}
if (!dequal(cleanData, defaultData)) {
onUpdate(cleanData);
}
onClose();
}, [defaultData, onUpdate, onClose, data]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
useEffect(() => {
nameField.current.select();
}, []);
if (step && step.type === StepTypes.DELETE) {
return (
<DeleteStep
title={t('common.deleteBoard', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisBoard')}
buttonContent={t('action.deleteBoard')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
}
return (
<>
<Popup.Header>
{t('common.editBoard', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={nameField}
name="name"
value={data.name}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive content={t('action.save')} />
</Form>
<Button
content={t('action.delete')}
className={styles.deleteButton}
onClick={handleDeleteClick}
/>
</Popup.Content>
</>
);
});
EditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(EditStep);
@@ -0,0 +1,19 @@
:global(#app) {
.deleteButton {
bottom: 12px;
box-shadow: 0 1px 0 #cbcccc;
position: absolute;
right: 9px;
}
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}
+3
View File
@@ -0,0 +1,3 @@
import Boards from './Boards';
export default Boards;
+248
View File
@@ -0,0 +1,248 @@
import pick from 'lodash/pick';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../lib/popup';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import BoardMembershipsStep from '../BoardMembershipsStep';
import LabelsStep from '../LabelsStep';
import DueDateEditStep from '../DueDateEditStep';
import TimerEditStep from '../TimerEditStep';
import CardMoveStep from '../CardMoveStep';
import DeleteStep from '../DeleteStep';
import styles from './ActionsPopup.module.scss';
const StepTypes = {
USERS: 'USERS',
LABELS: 'LABELS',
EDIT_DUE_DATE: 'EDIT_DUE_DATE',
EDIT_TIMER: 'EDIT_TIMER',
MOVE: 'MOVE',
DELETE: 'DELETE',
};
const ActionsStep = React.memo(
({
card,
projectsToLists,
boardMemberships,
currentUserIds,
labels,
currentLabelIds,
onNameEdit,
onUpdate,
onMove,
onTransfer,
onDelete,
onUserAdd,
onUserRemove,
onBoardFetch,
onLabelAdd,
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelDelete,
onClose,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleEditNameClick = useCallback(() => {
onNameEdit();
onClose();
}, [onNameEdit, onClose]);
const handleUsersClick = useCallback(() => {
openStep(StepTypes.USERS);
}, [openStep]);
const handleLabelsClick = useCallback(() => {
openStep(StepTypes.LABELS);
}, [openStep]);
const handleEditDueDateClick = useCallback(() => {
openStep(StepTypes.EDIT_DUE_DATE);
}, [openStep]);
const handleEditTimerClick = useCallback(() => {
openStep(StepTypes.EDIT_TIMER);
}, [openStep]);
const handleMoveClick = useCallback(() => {
openStep(StepTypes.MOVE);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
const handleDueDateUpdate = useCallback(
(dueDate) => {
onUpdate({
dueDate,
});
},
[onUpdate],
);
const handleTimerUpdate = useCallback(
(timer) => {
onUpdate({
timer,
});
},
[onUpdate],
);
if (step) {
switch (step.type) {
case StepTypes.USERS:
return (
<BoardMembershipsStep
items={boardMemberships}
currentUserIds={currentUserIds}
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
onBack={handleBack}
/>
);
case StepTypes.LABELS:
return (
<LabelsStep
items={labels}
currentIds={currentLabelIds}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onDelete={onLabelDelete}
onBack={handleBack}
/>
);
case StepTypes.EDIT_DUE_DATE:
return (
<DueDateEditStep
defaultValue={card.dueDate}
onUpdate={handleDueDateUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_TIMER:
return (
<TimerEditStep
defaultValue={card.timer}
onUpdate={handleTimerUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.MOVE:
return (
<CardMoveStep
projectsToLists={projectsToLists}
defaultPath={pick(card, ['projectId', 'boardId', 'listId'])}
onMove={onMove}
onTransfer={onTransfer}
onBoardFetch={onBoardFetch}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.DELETE:
return (
<DeleteStep
title={t('common.deleteCard', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisCard')}
buttonContent={t('action.deleteCard')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
default:
}
}
return (
<>
<Popup.Header>
{t('common.cardActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleEditNameClick}>
{t('action.editTitle', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleUsersClick}>
{t('common.members', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleLabelsClick}>
{t('common.labels', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditDueDateClick}>
{t('action.editDueDate', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditTimerClick}>
{t('action.editTimer', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleMoveClick}>
{t('action.moveCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteCard', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
ActionsStep.propTypes = {
/* eslint-disable react/forbid-prop-types */
card: PropTypes.object.isRequired,
projectsToLists: PropTypes.array.isRequired,
boardMemberships: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
currentLabelIds: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
onNameEdit: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onBoardFetch: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);
@@ -0,0 +1,11 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}

Some files were not shown because too many files have changed in this diff Show More