Compare commits

...

237 Commits

Author SHA1 Message Date
nephayr 01535ecf51 cover.jpg changed for finalblast CI 1 year ago
iMarKoLiGa 1217969e22
feat: Ability to configure OIDC claims source (#888)
Closes #884
1 year ago
Ahmed e410e21363
feat: Add Yemeni Arabic translation (#880) 1 year ago
Maksim Eltyshev a0f584e814 chore: Update version 1 year ago
Maksim Eltyshev 135f5eccb5 chore: Bump express and related dependencies 1 year ago
Maksim Eltyshev 10001db8cf chore: Update dependencies 1 year ago
Maksim Eltyshev 7ee351fe30 fix: Preserve extension for attachments with long filename
Closes #77
1 year ago
Maksim Eltyshev 73746b83d1 chore: Bump custom-ui css 1 year ago
Aline Fauquette f9a7b38ce9
feat: Change update strategy in case of persistence in Helm (#876) 1 year ago
dependabot[bot] 1f261fe7b6
chore(deps): Bump micromatch from 4.0.7 to 4.0.8 in /client (#879)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 80bee30463
chore(deps): Bump serve-static and express in /client (#878)
Bumps [serve-static](https://github.com/expressjs/serve-static) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] a9957481b6
chore(deps): Bump send and express in /client (#877)
Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 36b05a5478
chore(deps): Bump webpack from 5.91.0 to 5.94.0 in /client (#864)
Bumps [webpack](https://github.com/webpack/webpack) from 5.91.0 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.91.0...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] af91512abf
chore(deps): Bump micromatch from 4.0.7 to 4.0.8 (#874)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Maël Gangloff 8fd0f682d9
feat: Google Chat notifications (#867) 1 year ago
Maksim Eltyshev 9699fbe76a feat: Additional httpOnly token for enhanced security in browsers 1 year ago
Aurélien Troncy 4176a62f1a
fix: Update French translation (#862) 1 year ago
Tyler Sweat c12f0844d7
fix: Update Spanish translation (#861) 1 year ago
Aurélien Troncy e6644eb745
feat: Ability to show detailed auth errors, set to false by default (#860) 1 year ago
Blyamur b2e1fba9a0
fix: Update Russian translation (#857) 1 year ago
Maksim Eltyshev 818ae4a6df chore: Update version 1 year ago
Aurélien Troncy 0de0e809ac
fix: Update French translation (#849) 1 year ago
Maksim Eltyshev da727c0a0b chore: Generate separate file to store version
Closes #848
1 year ago
Ken 4841a65c77
feat: Add Traditional Chinese translation (#846) 1 year ago
Maksim Eltyshev 0ec4b619d9 chore: Update version 1 year ago
Maksim Eltyshev 12f05adde7 fix: Fix styles, refactoring 1 year ago
Arkadiusz Dzięgiel c594e8bd71
feat: Colorize due date and make it toggleable (#845) 1 year ago
Maksim Eltyshev f84166406f ref: Refactoring 1 year ago
Fayad 82b2ba298d
feat: Improvements for mobile version (#776) 1 year ago
jaycoolslm 38b6805ba1
fix: Update healthcheck parameters in docker-compose-dev.yml (#834) 1 year ago
dependabot[bot] 6a7a582045
chore(deps): Bump socket.io-parser from 3.3.3 to 3.3.4 in /client (#833)
Bumps [socket.io-parser](https://github.com/Automattic/socket.io-parser) from 3.3.3 to 3.3.4.
- [Release notes](https://github.com/Automattic/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/3.3.4/CHANGELOG.md)
- [Commits](https://github.com/Automattic/socket.io-parser/compare/3.3.3...3.3.4)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Blyamur c53a00b4ba
fix: Update Russian translation (#832) 1 year ago
Blyamur 52103c775b
fix: Update Russian translation (#831) 1 year ago
Maksim Eltyshev eeb1bd80cc chore: Update version 1 year ago
dependabot[bot] e6b8538863
chore(deps): Bump ws, engine.io and socket.io-adapter in /server (#830)
Bumps [ws](https://github.com/websockets/ws), [engine.io](https://github.com/socketio/engine.io) and [socket.io-adapter](https://github.com/socketio/socket.io-adapter). These dependencies needed to be updated together.

Updates `ws` from 8.11.0 to 8.17.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.11.0...8.17.1)

Updates `engine.io` from 6.5.4 to 6.5.5
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/6.5.5/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.5.4...6.5.5)

Updates `socket.io-adapter` from 2.5.4 to 2.5.5
- [Release notes](https://github.com/socketio/socket.io-adapter/releases)
- [Changelog](https://github.com/socketio/socket.io-adapter/blob/2.5.5/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-adapter/compare/2.5.4...2.5.5)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
- dependency-name: engine.io
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Maksim Eltyshev 9dc38932fb feat: Languages with country codes 1 year ago
Aurélien Troncy e9b4a4adfc
fix: Disable x-powered-by header (#829) 1 year ago
Mazyar Yousefiniyae shad 76c78c7252
feat: Add Persian translation (#828) 1 year ago
Maksim Eltyshev 04f52a63f2 chore: Update version 1 year ago
Maksim Eltyshev 6b16a104b1 docs: Update docker pulls counter 1 year ago
Maksim Eltyshev aff853c602 ref: Refactoring, fix linting 1 year ago
aleb_the_flash ad2966c5d6
feat: Improve OIDC support for strict providers (#824) 1 year ago
Maksim Eltyshev 8d74cc1732 chore: Update version 1 year ago
Emmanuel Guyot ea94fe8bf3
fix: Order users by name (#806) 1 year ago
Αλέξανδρος 6052f8999f
fix: Fix application crashing when webhook is unavailable (#815) 1 year ago
ItzAndriss 67d1b1c194
fix: Update Hungarian translation (#823) 1 year ago
Stephan Michard b3cb8405f5
fix: Update German translation (#821) 1 year ago
Maksim Eltyshev 7acaec6d04 fix: Fix incorrect related data when transferring card
Closes #431, closes #803
1 year ago
Maksim Eltyshev 72896a66b3 chore: Update version 2 years ago
Maksim Eltyshev 4be4783251 fix: Fix icon spacing for action buttons 2 years ago
HannesOberreiter 5c6c6a0bcf
feat: Add copy link action to card modal (#804) 2 years ago
HannesOberreiter ba178d66fa
docs: Add types to webhook function (#796) 2 years ago
HannesOberreiter 270ce29f87
feat: Add custom user agent with base url to webhook (#799) 2 years ago
HannesOberreiter de1ed3c784
ref: Define all possible event types in webhook module (#795) 2 years ago
Maksim Eltyshev caf8790f28 chore: Update version 2 years ago
Sarah Soo 81436b373d
feat: Ability to specify existing secrets for values in Helm (#791)
Closes #790
2 years ago
NathanVss cc1e886a31
feat: Ability to allow everyone to create projects (#787) 2 years ago
Maksim Eltyshev f0e9fe7904 chore: Update version 2 years ago
Gavin Mogan 5fcea5d651
fix: Handle WEBHOOKS env variable being unset (#785)
Closes #784
2 years ago
Maksim Eltyshev a963d85a66 chore: Update version 2 years ago
Samuel 04b42decef
meta: Add issue templates (#782) 2 years ago
Maksim Eltyshev c065566c15 feat: Webhooks configuration, all events support, refactoring 2 years ago
HannesOberreiter 3779bdb053
feat: Events via webhook (#771)
Closes #215, closes #656
2 years ago
HannesOberreiter 4124ab17d2
fix: Focus end of field when editing (#779) 2 years ago
Maksim Eltyshev 0e3bc92a61 chore: Update version 2 years ago
Maksim Eltyshev b8d7e713b9 chore: Update dependencies 2 years ago
Emmanuel Guyot 017eee8302
fix: Use server base path for socket connection (#772)
Closes #768
2 years ago
Mitch Harvey 1b28892cf5
fix: Fix Helm template for dburl secret (#770) 2 years ago
Maksim Eltyshev 013ea1b869 chore: Update version 2 years ago
Blyamur 4975fa2eeb
fix: Update Russian translation (#767) 2 years ago
Blyamur 3e28ad87c7
fix: Update Russian translation (#766) 2 years ago
Smiley3112 2d6666d693
feat: Add SMTP_NAME environment variable (#761)
Closes #758
2 years ago
Maksim Eltyshev 4cbd2f85c7 chore: Update version 2 years ago
Maksim Eltyshev fae9de4e1c fix: Prevent loading fonts from third-party resources
Closes #538, closes #712, closes #742, closes #755
2 years ago
IT Creativity + Art Team 89c1ed71e1
feat: Add Bulgarian translation (#753) 2 years ago
Mitch Harvey 1725f3cb8a
feat: Ability to specify existing secret as database url (#751)
Closes #750
2 years ago
Christopher Greaves f7be49e402
docs: Update Kubernetes README (#740) 2 years ago
Maksim Eltyshev c8d732d936 chore: Update version 2 years ago
Maksim Eltyshev 5a32b6327c fix: Fix actions with members
Closes #737
2 years ago
Emmanuel Guyot 8488105810
feat: Search in card descriptions (#729) 2 years ago
Niccolò Pedrini 57ebfa51d8
fix: Update Italian translation (#727) 2 years ago
Maksim Eltyshev 40170fdf90 chore: Update version 2 years ago
leroyloren 1629f62d09
fix: Update Czech translation (#725) 2 years ago
Maksim Eltyshev fff0552081 chore: Update dependencies
Closes #726
2 years ago
Maksim Eltyshev e792cb26b1 chore: Update version 2 years ago
Maksim Eltyshev 7ee2d76be4 feat: Display avatar next to user name in top bar 2 years ago
Emmanuel Guyot eb56b2147b
feat: Filter cards by keyword with advanced capabilities (#713)
Closes #706
2 years ago
Maksim Eltyshev 8747aa59de fix: Fix title of sort menu item 2 years ago
Samuel 934dcdf39b
feat: Sort cards within list (#717)
Closes #390
2 years ago
Felipe 2c84316fe0
fix: Limit amount of displayed board members (#715)
Closes #617
2 years ago
Daniel Hiller 03825c3bed CI: Fix dev Build 2 years ago
ItzAndriss 200ea3bbc0
feat: Add Hungarian translation (#590) 2 years ago
Robson Ventura 5b4b0cd635
feat: Development environment with docker compose (#709) 2 years ago
Gavin Mogan a1323cd516
feat: Set labels when building docker image 2 years ago
Maksim Eltyshev 92f75789fa chore: Update version 2 years ago
Maksim Eltyshev 8a8c1fee0c fix: Fix error output when sending email or message to Slack 2 years ago
Maksim Eltyshev 16499052f7 chore: Update version 2 years ago
HannesOberreiter cc32daa009
feat: Display clickable links in tasks (#694)
Closes #330
2 years ago
Stan M ea1b3b7f92
feat: Ability to run with read-only root filesystem in Helm (#695) 2 years ago
Maksim Eltyshev 4e2863faa7 feat: Automatic logout when session expires
Closes #693
2 years ago
Maksim Eltyshev b46fb43e6f chore: Cleanup 2 years ago
Maksim Eltyshev bcacfa8fe2 Merge branch 'GlitchSecure-slack-bot-integration' 2 years ago
Niccolò Pedrini 4df2becd6f fix: Update Italian translation (#688) 2 years ago
Matthieu Bollot 2990ea593a feat: Slack bot notifications (#676) 2 years ago
Blyamur b976673826 fix: Update Russian translation (#686) 2 years ago
leroyloren 16e034efcb fix: Update Czech translation (#679) 2 years ago
Maksim Eltyshev 6712625c3f chore: Update version 2 years ago
Matthieu Bollot b5bbf6a6a4 feat: Add ability to duplicate card (#668) 2 years ago
Brian Stinson 3d3e7761f5 fix: Allow specifying external dburl in Helm (#672) 2 years ago
piero fa56b9c01f fix: Update HorizontalPodAutoscaler version and spec in chart (#666)
Closes #667
2 years ago
Maksim Eltyshev 882213784f chore: Update version 2 years ago
Maksim Eltyshev 251bd793f3 fix: Fix OIDC authentication error when redirecting from another tab
Closes #650
2 years ago
Maksim Eltyshev 088b0e760b fix: Fix scrollbar styles 2 years ago
Maksim Eltyshev cc1fa1cc3b chore: Update version 2 years ago
Edouard bcd3ea86e8 feat: SMTP integration and email notifications (#631) 2 years ago
Maksim Eltyshev 0176650f67 fix: Add date-fns locale to Portuguese translation 2 years ago
Jeffrey f3963aded3 feat: Add Dutch translation (#644) 2 years ago
Reuben Hu bbe720481a feat: Add Indonesian translation (#643) 2 years ago
IV16SL 04f9030474 fix: Fix Chinese translation (#641) 2 years ago
Chris 6d135be859 feat: Add Portuguese translation (#640) 2 years ago
Maksim Eltyshev 63cbe7063e docs: Update features, add contributors 2 years ago
Maksim Eltyshev 7985bea2b8 chore: Update version 2 years ago
Maksim Eltyshev 733abccaa9 fix: Fix case sensitivity of default admin environment variables 2 years ago
Daniel Hiller 119cce82b4 fix: Container build windows 2 years ago
Maksim Eltyshev 988e70ec40 fix: Fix nullable boolean inputs 2 years ago
Daniel Hiller 5e3fca3771 Update build-and-push-docker-image-dev.yml 2 years ago
Maksim Eltyshev 6d9ce56f4a fix: Include due date when importing from Trello
Closes #598
2 years ago
Daniel Hiller 3c8c04fd37 ci: update workflow 2 years ago
Daniel Hiller 790a1d5cc9 feat: Add healthcheck for docker deployments
fix: Missing healthcheck.js

fix: missing healthcheck.js

fix: HEALTHCHECK command
2 years ago
Maksim Eltyshev 4b352fd878 fix: Remove overflow of attachment dropzone 2 years ago
Maksim Eltyshev a196f2ef11 chore: Update version 2 years ago
Maksim Eltyshev 6c65d135f7 feat: Add ability to enforce SSO
Closes #543, closes #545
2 years ago
Maksim Eltyshev a1a1e9a86a chore: Update version 2 years ago
Maksim Eltyshev 0d39a7567f feat: Add ability to map OIDC attributes and ignore username
Closes #554
2 years ago
Maksim Eltyshev 76a18d06d6 chore: Update version 2 years ago
Maksim Eltyshev ce52fc5af7 fix: Fix images becoming black and white when resizing
Closes #574, closes #585
2 years ago
Maksim Eltyshev 0a2863a79c chore: Bump sharp version 2 years ago
Niccolò Pedrini 39e320fe4b
fix: Update Italian translation (#688) 2 years ago
Matthieu Bollot 2f3dfe775e
feat: Slack bot notifications (#676) 2 years ago
Blyamur 22964cb375
fix: Update Russian translation (#686) 2 years ago
leroyloren fd89cf7f2b
fix: Update Czech translation (#679) 2 years ago
Maksim Eltyshev 5acf84c20b chore: Update version 2 years ago
Matthieu Bollot 6cd9da844f
feat: Add ability to duplicate card (#668) 2 years ago
Brian Stinson d7847fe89b
fix: Allow specifying external dburl in Helm (#672) 2 years ago
piero d033520927
fix: Update HorizontalPodAutoscaler version and spec in chart (#666)
Closes #667
2 years ago
Maksim Eltyshev d911030831 chore: Update version 2 years ago
Maksim Eltyshev 9d95ed6c41 fix: Fix OIDC authentication error when redirecting from another tab
Closes #650
2 years ago
Maksim Eltyshev 1a5a853fa3 fix: Fix scrollbar styles 2 years ago
Maksim Eltyshev bb358e01dd chore: Update version 2 years ago
Edouard 9f0fce098e
feat: SMTP integration and email notifications (#631) 2 years ago
Maksim Eltyshev 60ab09771d fix: Add date-fns locale to Portuguese translation 2 years ago
Jeffrey cb3ef783ba
feat: Add Dutch translation (#644) 2 years ago
Reuben Hu fe376b10c1
feat: Add Indonesian translation (#643) 2 years ago
IV16SL 786441cda0
fix: Fix Chinese translation (#641) 2 years ago
Chris 0d3cf0a32d
feat: Add Portuguese translation (#640) 2 years ago
Maksim Eltyshev b4e01d7256 docs: Update features, add contributors 2 years ago
Maksim Eltyshev 73a641b935 chore: Update version 2 years ago
Maksim Eltyshev 6802a0dc69 fix: Fix case sensitivity of default admin environment variables 2 years ago
Daniel Hiller 81d6c43cd9 fix: Container build windows 2 years ago
Maksim Eltyshev ca6a1a14f5 fix: Fix nullable boolean inputs 2 years ago
Daniel Hiller c096444acb
Update build-and-push-docker-image-dev.yml 2 years ago
Maksim Eltyshev 51d05f3cd3 fix: Include due date when importing from Trello
Closes #598
2 years ago
Daniel Hiller 221da3c455 ci: update workflow 2 years ago
Daniel Hiller 92098e136c feat: Add healthcheck for docker deployments
fix: Missing healthcheck.js

fix: missing healthcheck.js

fix: HEALTHCHECK command
2 years ago
Maksim Eltyshev fd5c33dd5d fix: Remove overflow of attachment dropzone 2 years ago
Maksim Eltyshev 0c1c7e2192 chore: Update version 2 years ago
Maksim Eltyshev b8d262f745 feat: Add ability to enforce SSO
Closes #543, closes #545
2 years ago
Maksim Eltyshev a1fd694248 chore: Update version 2 years ago
Maksim Eltyshev 634d6ceab1 feat: Add ability to map OIDC attributes and ignore username
Closes #554
2 years ago
Maksim Eltyshev 31d4d5f38d chore: Update version 2 years ago
Maksim Eltyshev 3f99438402 fix: Fix images becoming black and white when resizing
Closes #574, closes #585
2 years ago
Maksim Eltyshev 7e7727e3e1 chore: Bump sharp version 2 years ago
GlitchWitch 273d994750
Remove unused code and comments 2 years ago
Brad Bahls 86d21d6abd
updated to use currentUser.name for messages 2 years ago
GlitchWitch a9c200cc3b
Merge branch 'master' into slack-bot-integration 2 years ago
GlitchWitch 3fedc14062
Rename custom.js -> slack.js 2 years ago
GlitchWitch c42e1fb5dd
Add slack variables to docker-compose 2 years ago
GlitchWitch 9961340118
Replace plankaProdUrl with one set by environment. Remove hardcoded channel and unused variables. 2 years ago
Brad Bahls f54bd22757
removed test webhook url 2 years ago
Brad Bahls 1043dacd67
added new custom service with slack integration helper functions; added axios package; added notifications for card create, delete, and update (move); added notifications for comment create 2 years ago
Daniel Hiller 616304a1fa Update branch 2 years ago
Daniel Hiller 1e01148138 ci: Add retry on failure 2 years ago
Daniel Hiller ede56e1ae3 ci: Switch to matrix workflow 2 years ago
Maksim Eltyshev b2e0c45539 chore: Update version 2 years ago
Maksim Eltyshev 389648b546 fix: Add missing date formats to Ukrainian translation 2 years ago
Denis Polishchuk f4789c28db
feat: Add Ukrainian translation (#571) 2 years ago
ossdate 668092de7d
fix: Update Chinese translation (#569) 2 years ago
Yu Inoue 920f9c097d
fix: Display year when date is outside of current year (#567) 2 years ago
Maksim Eltyshev da63598de3 chore: Update version 2 years ago
Gilberto Vidal 517cb34cfe
fix: Fix eslint rules for Windows (#508) 2 years ago
NavyStack 079b8eba53
fix: Fine-tune Korean and Japanese translations, fix full stops (#561) 2 years ago
Maksim Eltyshev af02cdd8bf docs: Little readme fixes 2 years ago
Maksim Eltyshev e1bc189b88 chore: Bump chart-releaser-action version 2 years ago
Maksim Eltyshev 5f2723623a chore: Update version 2 years ago
Maksim Eltyshev fee6841d0a fix: Attempt to fix error that occurs during reconnection 2 years ago
Maksim Eltyshev ca6fb3b962 fix: Add state parameter to OIDC authorization url
Closes #558
2 years ago
Maksim Eltyshev 28c3f28e01 fix: Add issuer to OIDC callback parameters
Closes #562
2 years ago
Maksim Eltyshev 80d12aaeeb fix: Fix Korean translation linting 2 years ago
NavyStack 89af209a2e
fix: Update Korean translation (#560) 2 years ago
Maksim Eltyshev f1fed3e533 build: Stop using base image 2 years ago
Maksim Eltyshev eddeb96b16 fix: Fix Romanian translation linting 2 years ago
Maksim Eltyshev 9e8ff3b579 chore: Revert remark-gfm update 2 years ago
Maksim Eltyshev fc1278011c build: Use pnpm import before installing dependencies 2 years ago
Andrei Covali 24163463fc
feat: Add Romanian translation (#555) 2 years ago
Yu Inoue fdef87e3b9
fix: Add support for OIDC configuration thru Helm (#550)
Closes #548
2 years ago
Maksim Eltyshev 964498dbaa chore: Use Node v18 2 years ago
Maksim Eltyshev 5b519cb62f chore: Update version 2 years ago
Yu Inoue 11fc3b4485
fix: Separate service port and container port in Helm (#549) 2 years ago
Maksim Eltyshev 37c0b59f82 chore: Update vips version 2 years ago
Maksim Eltyshev 239611ad51 chore: Update dependencies 2 years ago
Yu Inoue 0575e665fe
fix: Fix time format in Japanese locale (#546)
Closes #542
2 years ago
Maksim Eltyshev 019766dced chore: Update version 2 years ago
Maksim Eltyshev 6dc9e4ed99 fix: Disable role change when OIDC roles are not ignored 2 years ago
Balthasar Hofer d4b64b90fc
feat: Add ability to ignore roles when logging in with SSO (#534)
Closes #533
2 years ago
Daniel Hiller 1a49826b85 docs: adjust filenames to match the docs 2 years ago
Daniel Hiller 815643eb2c ci: ignore some paths on push 2 years ago
Daniel Hiller bcc4c0d923 chore: update version 2 years ago
Daniel Hiller cf7810b067 fix: add admin user variables 2 years ago
Maksim Eltyshev f1c72df8b6 chore: Update version 2 years ago
Maksim Eltyshev 5056d38848 fix: Fix logging out with invalid token 2 years ago
Maksim Eltyshev 3ef5b3ead8 fix: Make default admin environment variables optional
Closes #526
2 years ago
Vishnu Kaushik ded58dc8b0
fix: Add task counter in card modal (#525)
Closes #467
2 years ago
Maksim Eltyshev d8ee1ecdd1 ci: Do not mark released chart as latest 2 years ago
Maksim Eltyshev fc2f4cd5d3 chore: Update version 2 years ago
Maksim Eltyshev 7ef457157e fix: Fallback to query if there is no fragment 2 years ago
Maksim Eltyshev 23ee815200 chore: Update version 2 years ago
Maksim Eltyshev 40c04c35ff ref: Refactoring 2 years ago
Lorenz Brun 743f2956c8
feat: Improve OIDC SSO (#524)
The OIDC implementation merged in https://github.com/plankanban/planka/pull/491 is flawed for multiple reasons.

It assumes that the access_token returned by the IDP has to be a JWT parseable by the RP which is not the case [1].
Many major IDPs do issue tokens which are not JWTs and RPs should not rely on the contents of these at all.
The only signed token which has a standardized format for direct RP consumption is the OIDC ID token (id_token), but this by default doesn't contain many claims, especially role claims are omitted from them by default for size reasons. To get these additional claims into the ID token, one needs an IDP with support for the "claims" parameter.

It requires manual specification of the JWKS URL which is mandatory in any OIDC discovery document and thus never needs to be manually specified.

It also makes the questionable decision to use a client-side code flow with PKCE where a normal code flow would be much more appropriate as all user data is processed in the backend which can securely hold a client secret (confidential client). This has far wider IDP support, is safer (due to direct involvement of the IDP in obtaining user information) and doesn't require working with ID tokens and claim parameters.

By using a server-side code flow we can also offload most complexity to the server alone, no longer requiring an additional OIDC library on the web client.

Also silent logout doesn't work on most IDPs for security reasons, one needs to actually redirect the user over to the IDP, which then prompts them once more if they actually want to log out.

This implementation should work with any OIDC-compliant IDP and even OAuth 2.0-only IDPs as long as they serve and OIDC discovery document.

[1] rfc-editor.org/rfc/rfc6749#section-5.1
2 years ago
Maksim Eltyshev 5bf5db27d8 chore: Update chart version 2 years ago
Maksim Eltyshev 667c5daf25 chore: Update version 2 years ago
Maksim Eltyshev 6662b0a717 fix: Fix order of checks when logging in 2 years ago
Maksim Eltyshev 348bf4d6c4 Merge branch 'master' of github.com:gorrilla10101/planka 2 years ago
Maksim Eltyshev 3ac2d289d0 chore: Update version 2 years ago
Maksim Eltyshev 3bc73c43ea chore: Bump sails version 2 years ago
Maksim Eltyshev 8e0c60f5be fix: OIDC finalization and refactoring 2 years ago
gorrilla10101 8a4bdbd1fe
Merge branch 'plankanban:master' into master 2 years ago
Jeffrey 5bf42be786 dynamic environment variables 2 years ago

@ -0,0 +1,53 @@
name: "🐛 Bug Report"
description: Report a bug found while using Planka
title: "[Bug]: "
labels: ["Type: Bug", "Status: Triage"]
body:
- type: dropdown
id: issue-type
attributes:
label: Where is the problem occurring?
description: Select the part of the application where you encountered the issue.
options:
- "I encountered the problem while using the application (Frontend)"
- "I encountered the problem while interacting with the server (Backend)"
- "I'm not sure"
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Brave
- Chrome
- Firefox
- Microsoft Edge
- Safari
- Other
- type: textarea
id: current-behavior
attributes:
label: Current behaviour
description: A description of what is currently happening, including screenshots and other useful information (**DO NOT INCLUDE PRIVATE INFORMATION**).
placeholder: Currently...
validations:
required: true
- type: textarea
id: desired-behavior
attributes:
label: Desired behaviour
description: A clear description of what you think should happen.
placeholder: In this situation, I expected ...
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Clearly describe which steps or actions you have taken to arrive at the problem. If you have some experience with the code, please link to the specific pieces of code.
placeholder: I did X, then Y, before arriving at Z, when ERROR ...
validations:
required: true
- type: textarea
id: other
attributes:
label: Other information
description: Any other details?

@ -0,0 +1,33 @@
name: "✨ Feature Request"
description: Suggest a feature or enhancement to improve Planka.
labels: ["Type: Idea"]
body:
- type: dropdown
id: idea-type
attributes:
label: Is this a feature for the backend or frontend?
multiple: true
options:
- Backend
- Frontend
validations:
required: true
- type: textarea
id: feature
attributes:
label: What would you like?
description: A clear description of the feature or enhancement wanted.
placeholder: I'd like to be able to...
validations:
required: true
- type: textarea
id: reason
attributes:
label: Why is this needed?
description: A clear description of why this would be useful to have.
placeholder: I want this because...
- type: textarea
id: other
attributes:
label: Other information
placeholder: Any other details?

@ -40,5 +40,5 @@ jobs:
build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }}
push: true
tags: |
ghcr.io/plankanban/planka:base-latest
ghcr.io/plankanban/planka:base-${{ env.ALPINE_VERSION }}
ghcr.io/${{ github.repository }}:base-latest
ghcr.io/${{ github.repository }}:base-${{ env.ALPINE_VERSION }}

@ -1,11 +1,20 @@
# https://docs.docker.com/build/ci/github-actions/multi-platform/
name: Build and push Docker DEV image
on:
push:
paths-ignore:
- '.github/**'
- 'charts/**'
- 'docker-*.sh'
- '*.md'
branches: [master]
workflow_dispatch:
env:
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
jobs:
build-and-push-docker-image-dev:
build:
runs-on: self-hosted
steps:
- name: Checkout
@ -24,11 +33,21 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=raw,value=dev
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/plankanban/planka:dev
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

@ -31,12 +31,23 @@ jobs:
result-encoding: string
script: return context.payload.release.tag_name.replace('v', '')
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
images: |
name=ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.set-version.outputs.result }}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v4
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 }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

@ -35,8 +35,9 @@ jobs:
done
- name: Run chart-releaser for stable
uses: helm/chart-releaser-action@v1.5.0
uses: helm/chart-releaser-action@v1.6.0
with:
charts_dir: charts
mark_as_latest: false
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

@ -1,4 +1,2 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

@ -1,35 +1,44 @@
FROM ghcr.io/plankanban/planka:base-latest as server-dependencies
FROM node:18-alpine as server-dependencies
RUN apk -U upgrade \
&& apk add build-base python3 \
--no-cache
WORKDIR /app
COPY server/package.json server/package-lock.json .
COPY server/package.json server/package-lock.json ./
RUN npm install npm@latest --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod
FROM node:lts AS client
WORKDIR /app
COPY client/package.json client/package-lock.json .
COPY client/package.json client/package-lock.json ./
RUN npm install npm@latest --global \
&& npm install pnpm --global \
&& pnpm import \
&& pnpm install --prod
COPY client .
RUN DISABLE_ESLINT_PLUGIN=true npm run build
FROM ghcr.io/plankanban/planka:base-latest
FROM node:18-alpine
RUN apk del vips-dependencies --purge
RUN apk -U upgrade \
&& apk add bash \
--no-cache
USER node
WORKDIR /app
COPY --chown=node:node start.sh .
COPY --chown=node:node server .
COPY --chown=node:node healthcheck.js .
RUN mv .env.sample .env
@ -44,4 +53,8 @@ VOLUME /app/private/attachments
EXPOSE 1337
CMD ["./start.sh"]
HEALTHCHECK --interval=10s --timeout=2s --start-period=15s \
CMD node ./healthcheck.js
CMD [ "bash", "start.sh" ]

@ -1,27 +1,22 @@
FROM node:lts-alpine
FROM node:18-alpine
ARG ALPINE_VERSION=3.16
ARG VIPS_VERSION=8.13.3
ARG VIPS_VERSION=8.14.5
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/ \
bash pkgconf \
libjpeg-turbo libexif librsvg cgif tiff libspng libimagequant \
--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 \
build-base gobject-introspection-dev meson \
libjpeg-turbo-dev libexif-dev librsvg-dev cgif-dev tiff-dev libspng-dev libimagequant-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 \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz | tar xJC /tmp \
&& cd /tmp/vips-${VIPS_VERSION} \
&& ./configure \
&& make \
&& make install-strip \
&& meson setup build-dir \
&& cd build-dir \
&& ninja \
&& ninja test \
&& ninja install \
&& rm -rf /tmp/vips-${VIPS_VERSION}

@ -1,7 +1,7 @@
# Planka
#### Elegant open source project tracking
#### Elegant open source project tracking.
![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)
![David (path)](https://img.shields.io/github/package-json/v/plankanban/planka) ![Docker Pulls](https://img.shields.io/badge/docker_pulls-4M%2B-%23066da5) ![GitHub](https://img.shields.io/github/license/plankanban/planka)
![](https://raw.githubusercontent.com/plankanban/planka/master/demo.gif)
@ -10,20 +10,18 @@
## 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
- Add card members, track time, set due dates, add attachments, write comments
- Markdown support in card description and comments
- Filter by members and labels
- Customize project background
- Customize project backgrounds
- Real-time updates
- User notifications
- Internationalization
- Internal notifications
- Multiple interface languages
- Single sign-on via OpenID Connect
## How to deploy Planka
There are 2 types of installation:
- [Without Docker](https://docs.planka.cloud/docs/installl-planka/Debian%20&%20Ubuntu) ([for Windows](https://docs.planka.cloud/docs/installl-planka/Windows))
- [Dockerized](https://docs.planka.cloud/docs/installl-planka/Docker%20Compose)
- [Automated installation](https://github.com/plankanban/planka-installer)
There are many ways to install Planka, [check them out](https://docs.planka.cloud/docs/intro).
For configuration, please see the [configuration section](https://docs.planka.cloud/docs/category/configuration).
@ -47,3 +45,7 @@ See the [development section](https://docs.planka.cloud/docs/Development).
## License
Planka is [AGPL-3.0 licensed](https://github.com/plankanban/planka/blob/master/LICENSE).
## Contributors
[![](https://contrib.rocks/image?repo=plankanban/planka)](https://github.com/plankanban/planka/graphs/contributors)

@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.2
version: 0.2.8
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.12.0"
appVersion: "1.22.0"
dependencies:
- alias: postgresql

@ -14,14 +14,22 @@ If you want to fully uninstall this chart including the data, follow [these step
## Usage
If you just want to spin up an instance using help, please see [these docs](https://docs.planka.cloud/docs/installation/kubernetes/helm_chart/). If you want to make changes to the chart locally, and deploy them, see the below section.
## Local Building and Using the Chart
The basic usage of the chart can be found below:
```bash
git clone https://github.com/Chris-Greaves/planka-helm-chart.git
cd planka-helm-chart
git clone https://github.com/plankanban/planka.git
cd planka/charts/planka
helm dependency build
export SECRETKEY=$(openssl rand -hex 64)
helm install planka . --set secretkey=$SECRETKEY
helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo"
```
> **NOTE:** The command `openssl rand -hex 64` is needed to create a random hexadecimal key for planka. On Windows you can use Git Bash to run that command.
@ -39,11 +47,19 @@ To access Planka externally you can use the following configuration
```bash
# HTTP only
helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo" \
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
# HTTPS
helm install planka . --set secretkey=$SECRETKEY \
--set admin_email="demo@demo.demo" \
--set admin_password="demo" \
--set admin_name="Demo Demo" \
--set admin_username="demo" \
--set ingress.enabled=true \
--set ingress.hosts[0].host=planka.example.dev \
--set ingress.tls[0].secretName=planka-tls \
@ -54,6 +70,17 @@ or create a values.yaml file like:
```yaml
secretkey: "<InsertSecretKey>"
# The admin section needs to be present for new instances of Planka, after the first start you can remove the lines starting with admin_. If you want the admin user to be unchangeable admin_email: has to stay
# After changing the config you have to run ```helm upgrade planka . -f values.yaml```
# Admin user
admin_email: "demo@demo.demo" # Do not remove if you want to prevent this user from being edited/deleted
admin_password: "demo"
admin_name: "Demo Demo"
admin_username: "demo"
# Admin user
# Ingress
ingress:
enabled: true
hosts:

@ -11,6 +11,10 @@ spec:
selector:
matchLabels:
{{- include "planka.selectorLabels" . | nindent 6 }}
{{- if .Values.persistence.enabled }}
strategy:
type: Recreate
{{- end }}
template:
metadata:
{{- with .Values.podAnnotations }}
@ -35,7 +39,7 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
containerPort: {{ .Values.service.containerPort | default 1337 }}
protocol: TCP
livenessProbe:
httpGet:
@ -55,14 +59,32 @@ spec:
- mountPath: /app/private/attachments
subPath: attachments
name: planka
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- mountPath: /app/logs
subPath: app-logs
name: emptydir
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if not .Values.postgresql.enabled }}
{{- if .Values.existingDburlSecret }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ .Values.existingDburlSecret }}
key: uri
{{- else }}
- name: DATABASE_URL
value: {{ required "If the included postgresql deployment is disabled you need to provide an existing secret in .Values.existingDburlSecret or define a Database URL in 'dburl'" .Values.dburl }}
{{- end }}
{{- else }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: planka-postgresql-svcbind-custom-user
key: uri
{{- end }}
- name: BASE_URL
{{- if .Values.baseUrl }}
value: {{ .Values.baseUrl }}
@ -72,9 +94,68 @@ spec:
value: http://localhost:3000
{{- end }}
- name: SECRET_KEY
{{- if .Values.existingSecretkeySecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecretkeySecret }}
key: key
{{- else }}
value: {{ required "A secret key needs to be generated using 'openssl rand -hex 64' and assigned to secretkey." .Values.secretkey }}
{{- end }}
- name: TRUST_PROXY
value: "0"
- name: DEFAULT_ADMIN_EMAIL
value: {{ .Values.admin_email }}
- name: DEFAULT_ADMIN_NAME
value: {{ .Values.admin_name }}
{{- if .Values.existingAdminCredsSecret }}
- name: DEFAULT_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: {{ .Values.existingAdminCredsSecret }}
key: username
- name: DEFAULT_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingAdminCredsSecret }}
key: password
{{- else }}
- name: DEFAULT_ADMIN_USERNAME
value: {{ .Values.admin_username }}
- name: DEFAULT_ADMIN_PASSWORD
value: {{ .Values.admin_password }}
{{- end }}
{{ range $k, $v := .Values.env }}
- name: {{ $k | quote }}
value: {{ $v | quote }}
{{- end }}
{{- if .Values.oidc.enabled }}
{{- $secretName := default (printf "%s-oidc" (include "planka.fullname" .)) .Values.oidc.existingSecret }}
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
key: clientId
name: {{ $secretName }}
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
key: clientSecret
name: {{ $secretName }}
- name: OIDC_ISSUER
value: {{ required "issuerUrl is required when configuring OIDC" .Values.oidc.issuerUrl | quote }}
- name: OIDC_SCOPES
value: {{ join " " .Values.oidc.scopes | default "openid profile email" | quote }}
{{- if .Values.oidc.admin.roles }}
- name: OIDC_ADMIN_ROLES
value: {{ join "," .Values.oidc.admin.roles | quote }}
{{- end }}
- name: OIDC_ROLES_ATTRIBUTE
value: {{ .Values.oidc.admin.rolesAttribute | default "groups" | quote }}
{{- if .Values.oidc.admin.ignoreRoles }}
- name: OIDC_IGNORE_ROLES
value: {{ .Values.oidc.admin.ignoreRoles | quote }}
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
@ -95,3 +176,7 @@ spec:
{{- else }}
emptyDir: {}
{{- end }}
{{- if .Values.securityContext.readOnlyRootFilesystem }}
- name: emptydir
emptyDir: {}
{{- end }}

@ -1,5 +1,5 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "planka.fullname" . }}
@ -17,12 +17,16 @@ spec:
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

@ -0,0 +1,17 @@
{{- if .Values.oidc.enabled }}
{{- if eq (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (not (empty .Values.oidc.existingSecret)) -}}
{{- fail "Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`" -}}
{{- end }}
{{- if (and (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (empty .Values.oidc.existingSecret)) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "planka.fullname" . }}-oidc
labels:
{{- include "planka.labels" . | nindent 4 }}
type: Opaque
data:
clientId: {{ .Values.oidc.clientId | b64enc | quote }}
clientSecret: {{ .Values.oidc.clientSecret | b64enc | quote }}
{{- end }}
{{- end }}

@ -17,6 +17,16 @@ fullnameOverride: ""
# Generate a secret using openssl rand -base64 45
secretkey: ""
## @param existingSecretkeySecret Name of an existing secret containing the session key string
## NOTE: Must contain key `key`
## NOTE: When it's set, the secretkey parameter is ignored
existingSecretkeySecret: ""
## @param existingAdminCredsSecret Name of an existing secret containing the admin username and password
## NOTE: Must contain keys `username` and `password`
## NOTE: When it's set, the `admin_username` and `admin_password` parameters are ignored
existingAdminCredsSecret: ""
# Base url for Planka. Will override `ingress.hosts[0].host`
# Defaults to `http://localhost:3000` if ingress is disabled.
baseUrl: ""
@ -46,6 +56,10 @@ securityContext: {}
service:
type: ClusterIP
port: 1337
## @param service.containerPort Planka HTTP container port
## If empty will default to 1337
##
containerPort: 1337
ingress:
enabled: false
@ -101,11 +115,84 @@ postgresql:
serviceBindings:
enabled: true
## Set this or existingDburlSecret if you disable the built-in postgresql deployment
dburl:
## @param existingDburlSecret Name of an existing secret containing a DBurl connection string
## NOTE: Must contain key `uri`
## NOTE: When it's set, the `dburl` parameter is ignored
##
existingDburlSecret: ""
## PVC-based data storage configuration
persistence:
enabled: false
# existingClaim: netbox-data
# storageClass: "-"
accessMode: ReadWriteOnce
size: 10Gi
## OpenID Identity Management configuration
##
## Example:
## ---------------
## oidc:
## enabled: true
## clientId: sxxaAIAxVXlCxTmc1YLHBbQr8NL8MqLI2DUbt42d
## clientSecret: om4RTMRVHRszU7bqxB7RZNkHIzA8e4sGYWxeCwIMYQXPwEBWe4SY5a0wwCe9ltB3zrq5f0dnFnp34cEHD7QSMHsKvV9AiV5Z7eqDraMnv0I8IFivmuV5wovAECAYreSI
## issuerUrl: https://auth.local/application/o/planka/
## admin:
## roles:
## - planka-admin
##
## ---------------
## NOTE: A minimal configuration requires setting `clientId`, `clientSecret` and `issuerUrl`. (plus `admin.roles` for administrators)
## ref: https://docs.planka.cloud/docs/Configuration/OIDC
##
oidc:
## @param oidc.enabled Enable single sign-on (SSO) with OpenID Connect (OIDC)
##
enabled: false
## OIDC credentials
## @param oidc.clientId A string unique to the provider that identifies your app.
## @param oidc.clientSecret A secret string that the provider uses to confirm ownership of a client ID.
##
## NOTE: Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`
##
clientId: ""
clientSecret: ""
## @param oidc.existingSecret Name of an existing secret containing OIDC credentials
## NOTE: Must contain key `clientId` and `clientSecret`
## NOTE: When it's set, the `clientId` and `clientSecret` parameters are ignored
##
existingSecret: ""
## @param oidc.issuerUrl The OpenID connect metadata document endpoint
##
issuerUrl: ""
## @param oidc.scopes A list of scopes required for OIDC client.
## If empty will default to `openid`, `profile` and `email`
## NOTE: Planka needs the email and name claims
##
scopes: []
## Admin permissions configuration
admin:
## @param oidc.admin.ignoreRoles If set to true, the admin roles will be ignored.
## It is useful if you want to use OIDC for authentication but not for authorization.
## If empty will default to `false`
##
ignoreRoles: false
## @param oidc.admin.rolesAttribute The name of a custom group claim that you have configured in your OIDC provider
## If empty will default to `groups`
##
rolesAttribute: groups
## @param oidc.admin.roles The names of the admin groups
##
roles: []
# - planka-admin

@ -1 +0,0 @@
REACT_APP_VERSION=1.12.0

45878
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -49,67 +49,76 @@
"**/*.test.js"
]
}
],
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"classnames": "^2.3.2",
"date-fns": "^2.29.3",
"classnames": "^2.5.1",
"date-fns": "^2.30.0",
"dequal": "^2.0.3",
"easymde": "^2.18.0",
"history": "^5.3.0",
"i18next": "^22.0.6",
"i18next-browser-languagedetector": "^7.0.1",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"initials": "^3.1.2",
"js-cookie": "^3.0.1",
"jwt-decode": "^3.1.2",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash": "^4.17.21",
"node-sass": "^8.0.0",
"photoswipe": "^5.3.3",
"nanoid": "^5.0.7",
"node-sass": "^9.0.0",
"photoswipe": "^5.4.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react": "18.2.0",
"react-app-rewired": "^2.2.1",
"react-beautiful-dnd": "^13.1.1",
"react-datepicker": "^4.8.0",
"react-dom": "^18.2.0",
"react-datepicker": "^4.25.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-i18next": "^12.0.0",
"react-i18next": "^15.0.2",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.3",
"react-oidc-context": "^2.2.2",
"react-photoswipe-gallery": "^2.2.2",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
"react-markdown": "^8.0.7",
"react-photoswipe-gallery": "^2.2.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.26.2",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"react-textarea-autosize": "^8.4.0",
"redux": "^4.2.0",
"react-textarea-autosize": "^8.5.3",
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-orm": "^0.16.2",
"redux-saga": "^1.2.1",
"remark-breaks": "^3.0.2",
"redux-saga": "^1.3.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^3.0.1",
"reselect": "^4.1.7",
"reselect": "^4.1.8",
"sails.io.js": "^1.2.1",
"semantic-ui-react": "^2.1.3",
"semantic-ui-react": "^2.1.5",
"socket.io-client": "^2.5.0",
"validator": "^13.7.0",
"whatwg-fetch": "^3.6.2",
"validator": "^13.12.0",
"whatwg-fetch": "^3.6.20",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.5.2",
"babel-preset-airbnb": "^5.0.0",
"chai": "^4.3.7",
"eslint": "^8.28.0",
"chai": "^4.5.0",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"react-test-renderer": "^18.2.0"
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.36.1",
"eslint-plugin-react-hooks": "^4.6.2",
"react-test-renderer": "18.2.0"
}
}

@ -3,7 +3,7 @@
<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="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<meta
name="description"

@ -23,10 +23,14 @@ createCard.failure = (localId, error) => ({
},
});
const handleCardCreate = (card) => ({
const handleCardCreate = (card, cardMemberships, cardLabels, tasks, attachments) => ({
type: ActionTypes.CARD_CREATE_HANDLE,
payload: {
card,
cardMemberships,
cardLabels,
tasks,
attachments,
},
});
@ -53,10 +57,43 @@ updateCard.failure = (id, error) => ({
},
});
const handleCardUpdate = (card) => ({
const handleCardUpdate = (card, isFetched, cardMemberships, cardLabels, tasks, attachments) => ({
type: ActionTypes.CARD_UPDATE_HANDLE,
payload: {
card,
isFetched,
cardMemberships,
cardLabels,
tasks,
attachments,
},
});
const duplicateCard = (id, card, taskIds) => ({
type: ActionTypes.CARD_DUPLICATE,
payload: {
id,
card,
taskIds,
},
});
duplicateCard.success = (localId, card, cardMemberships, cardLabels, tasks) => ({
type: ActionTypes.CARD_DUPLICATE__SUCCESS,
payload: {
localId,
card,
cardMemberships,
cardLabels,
tasks,
},
});
duplicateCard.failure = (id, error) => ({
type: ActionTypes.CARD_DUPLICATE__FAILURE,
payload: {
id,
error,
},
});
@ -89,11 +126,21 @@ const handleCardDelete = (card) => ({
},
});
const filterText = (boardId, text) => ({
type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
boardId,
text,
},
});
export default {
createCard,
handleCardCreate,
updateCard,
handleCardUpdate,
duplicateCard,
deleteCard,
handleCardDelete,
filterText,
};

@ -39,9 +39,19 @@ const initializeCore = (
},
});
const logout = () => ({
// TODO: with success?
initializeCore.fetchConfig = (config) => ({
type: ActionTypes.CORE_INITIALIZE__CONFIG_FETCH,
payload: {
config,
},
});
const logout = (invalidateAccessToken) => ({
type: ActionTypes.LOGOUT,
payload: {},
payload: {
invalidateAccessToken,
},
});
logout.invalidateAccessToken = () => ({

@ -60,6 +60,38 @@ const handleListUpdate = (list) => ({
},
});
const sortList = (id, data) => ({
type: ActionTypes.LIST_SORT,
payload: {
id,
data,
},
});
sortList.success = (list, cards) => ({
type: ActionTypes.LIST_SORT__SUCCESS,
payload: {
list,
cards,
},
});
sortList.failure = (id, error) => ({
type: ActionTypes.LIST_SORT__FAILURE,
payload: {
id,
error,
},
});
const handleListSort = (list, cards) => ({
type: ActionTypes.LIST_SORT_HANDLE,
payload: {
list,
cards,
},
});
const deleteList = (id) => ({
type: ActionTypes.LIST_DELETE,
payload: {
@ -94,6 +126,8 @@ export default {
handleListCreate,
updateList,
handleListUpdate,
sortList,
handleListSort,
deleteList,
handleListDelete,
};

@ -1,5 +1,12 @@
import ActionTypes from '../constants/ActionTypes';
const initializeLogin = (config) => ({
type: ActionTypes.LOGIN_INITIALIZE,
payload: {
config,
},
});
const authenticate = (data) => ({
type: ActionTypes.AUTHENTICATE,
payload: {
@ -21,12 +28,33 @@ authenticate.failure = (error) => ({
},
});
const authenticateUsingOidc = () => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE,
payload: {},
});
authenticateUsingOidc.success = (accessToken) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS,
payload: {
accessToken,
},
});
authenticateUsingOidc.failure = (error) => ({
type: ActionTypes.USING_OIDC_AUTHENTICATE__FAILURE,
payload: {
error,
},
});
const clearAuthenticateError = () => ({
type: ActionTypes.AUTHENTICATE_ERROR_CLEAR,
payload: {},
});
export default {
initializeLogin,
authenticate,
authenticateUsingOidc,
clearAuthenticateError,
};

@ -1,17 +1,17 @@
import http from './http';
import socket from './socket';
/* Actions */
const createAccessToken = (data, headers) => http.post('/access-tokens', data, headers);
const exchangeOidcToken = (accessToken, headers) =>
http.post('/access-tokens/exchange', { token: accessToken }, headers);
const createAccessToken = (data, headers) =>
http.post('/access-tokens?withHttpOnlyToken=true', data, headers);
const deleteCurrentAccessToken = (headers) =>
socket.delete('/access-tokens/me', undefined, headers);
const exchangeForAccessTokenUsingOidc = (data, headers) =>
http.post('/access-tokens/exchange-using-oidc?withHttpOnlyToken=true', data, headers);
const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', undefined, headers);
export default {
createAccessToken,
exchangeForAccessTokenUsingOidc,
deleteCurrentAccessToken,
exchangeOidcToken,
};

@ -57,6 +57,12 @@ const updateCard = (id, data, headers) =>
item: transformCard(body.item),
}));
const duplicateCard = (id, data, headers) =>
socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({
...body,
item: transformCard(body.item),
}));
const deleteCard = (id, headers) =>
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
...body,
@ -81,6 +87,7 @@ export default {
getCard,
updateCard,
deleteCard,
duplicateCard,
makeHandleCardCreate,
makeHandleCardUpdate,
makeHandleCardDelete,

@ -5,9 +5,11 @@ import Config from '../constants/Config';
const http = {};
// TODO: add all methods
['POST'].forEach((method) => {
['GET', 'POST', 'DELETE'].forEach((method) => {
http[method.toLowerCase()] = (url, data, headers) => {
const formData = Object.keys(data).reduce((result, key) => {
const formData =
data &&
Object.keys(data).reduce((result, key) => {
result.append(key, data[key]);
return result;
@ -17,6 +19,7 @@ const http = {};
method,
headers,
body: formData,
credentials: 'include',
})
.then((response) =>
response.json().then((body) => ({

@ -1,5 +1,6 @@
import http from './http';
import socket from './socket';
import root from './root';
import accessTokens from './access-tokens';
import users from './users';
import projects from './projects';
@ -20,6 +21,7 @@ import notifications from './notifications';
export { http, socket };
export default {
...root,
...accessTokens,
...users,
...projects,

@ -1,4 +1,5 @@
import socket from './socket';
import { transformCard } from './cards';
/* Actions */
@ -7,10 +8,33 @@ const createList = (boardId, data, headers) =>
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
const sortList = (id, data, headers) =>
socket.post(`/lists/${id}/sort`, data, headers).then((body) => ({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
}));
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
/* Event handlers */
const makeHandleListSort = (next) => (body) => {
next({
...body,
included: {
...body.included,
cards: body.included.cards.map(transformCard),
},
});
};
export default {
createList,
updateList,
sortList,
deleteList,
makeHandleListSort,
};

@ -0,0 +1,9 @@
import http from './http';
/* Actions */
const getConfig = (headers) => http.get('/config', undefined, headers);
export default {
getConfig,
};

@ -13,7 +13,7 @@ io.sails.environment = process.env.NODE_ENV;
const { socket } = io;
socket.path = `${Config.BASE_PATH}/socket.io`;
socket.path = `${Config.SERVER_BASE_PATH}/socket.io`;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 184 KiB

@ -1,6 +1,8 @@
:global(#app) {
.wrapper {
height: 100%;
max-height: 100vh;
max-width: 100vw;
position: fixed;
width: 100%;
z-index: -1;

@ -48,16 +48,6 @@
min-width: 100%;
}
.panel {
align-items: center;
display: flex;
margin-bottom: 20px;
}
.panelItem {
margin-right: 20px;
}
.wrapper {
margin: 0 20px;
}

@ -13,6 +13,7 @@ const BoardActions = React.memo(
labels,
filterUsers,
filterLabels,
filterText,
allUsers,
canEdit,
canEditMemberships,
@ -27,6 +28,7 @@ const BoardActions = React.memo(
onLabelUpdate,
onLabelMove,
onLabelDelete,
onTextFilterUpdate,
}) => {
return (
<div className={styles.wrapper}>
@ -46,6 +48,7 @@ const BoardActions = React.memo(
<Filters
users={filterUsers}
labels={filterLabels}
filterText={filterText}
allBoardMemberships={memberships}
allLabels={labels}
canEdit={canEdit}
@ -57,6 +60,7 @@ const BoardActions = React.memo(
onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete}
onTextFilterUpdate={onTextFilterUpdate}
/>
</div>
</div>
@ -71,6 +75,7 @@ BoardActions.propTypes = {
labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
@ -86,6 +91,7 @@ BoardActions.propTypes = {
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
};
export default BoardActions;

@ -1,12 +1,15 @@
:global(#app) {
.action {
align-items: center;
display: flex;
flex: 0 0 auto;
margin-right: 20px;
}
.actions {
align-items: center;
display: flex;
gap: 20px;
justify-content: flex-start;
margin: 20px 20px;
}

@ -1,7 +1,10 @@
import React, { useCallback } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup';
import { Input } from '../../lib/custom-ui';
import User from '../User';
import Label from '../Label';
@ -14,6 +17,7 @@ const Filters = React.memo(
({
users,
labels,
filterText,
allBoardMemberships,
allLabels,
canEdit,
@ -25,8 +29,17 @@ const Filters = React.memo(
onLabelUpdate,
onLabelMove,
onLabelDelete,
onTextFilterUpdate,
}) => {
const [t] = useTranslation();
const [isSearchFocused, setIsSearchFocused] = useState(false);
const searchFieldRef = useRef(null);
const cancelSearch = useCallback(() => {
onTextFilterUpdate('');
searchFieldRef.current.blur();
}, [onTextFilterUpdate]);
const handleRemoveUserClick = useCallback(
(id) => {
@ -42,9 +55,39 @@ const Filters = React.memo(
[onLabelRemove],
);
const handleSearchChange = useCallback(
(_, { value }) => {
onTextFilterUpdate(value);
},
[onTextFilterUpdate],
);
const handleSearchFocus = useCallback(() => {
setIsSearchFocused(true);
}, []);
const handleSearchKeyDown = useCallback(
(event) => {
if (event.key === 'Escape') {
cancelSearch();
}
},
[cancelSearch],
);
const handleSearchBlur = useCallback(() => {
setIsSearchFocused(false);
}, []);
const handleCancelSearchClick = useCallback(() => {
cancelSearch();
}, [cancelSearch]);
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
const LabelsPopup = usePopup(LabelsStep);
const isSearchActive = filterText || isSearchFocused;
return (
<>
<span className={styles.filter}>
@ -100,6 +143,25 @@ const Filters = React.memo(
</span>
))}
</span>
<span className={styles.filter}>
<Input
ref={searchFieldRef}
value={filterText}
placeholder={t('common.searchCards')}
icon={
isSearchActive ? (
<Icon link name="cancel" onClick={handleCancelSearchClick} />
) : (
'search'
)
}
className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
onFocus={handleSearchFocus}
onKeyDown={handleSearchKeyDown}
onChange={handleSearchChange}
onBlur={handleSearchBlur}
/>
</span>
</>
);
},
@ -109,6 +171,7 @@ Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
@ -121,6 +184,7 @@ Filters.propTypes = {
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
};
export default Filters;

@ -43,4 +43,36 @@
line-height: 20px;
padding: 2px 12px;
}
.search {
height: 30px;
margin: 0 12px;
transition: width 0.2s ease;
width: 280px;
@media only screen and (max-width: 797px) {
width: 220px;
}
input {
font-size: 13px;
}
}
.searchInactive {
color: #fff;
height: 24px;
width: 220px;
input {
background: rgba(0, 0, 0, 0.24);
border: none;
color: #fff !important;
font-size: 12px;
&::placeholder {
color: #fff;
}
}
}
}

@ -4,7 +4,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
width: 100%;
&::-webkit-scrollbar {

@ -85,7 +85,6 @@
height: 56px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
&:hover {
height: 38px;

@ -36,6 +36,7 @@ const ActionsStep = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
@ -76,6 +77,11 @@ const ActionsStep = React.memo(
openStep(StepTypes.MOVE);
}, [openStep]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
@ -207,6 +213,11 @@ const ActionsStep = React.memo(
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
{t('action.duplicateCard', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteCard', {
context: 'title',
@ -232,6 +243,7 @@ ActionsStep.propTypes = {
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,

@ -24,6 +24,7 @@ const Card = React.memo(
index,
name,
dueDate,
isDueDateCompleted,
stopwatch,
coverUrl,
boardId,
@ -41,6 +42,7 @@ const Card = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
@ -119,7 +121,7 @@ const Card = React.memo(
)}
{dueDate && (
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
<DueDate value={dueDate} size="tiny" />
<DueDate value={dueDate} isCompleted={isDueDateCompleted} size="tiny" />
</span>
)}
{stopwatch && (
@ -185,6 +187,7 @@ const Card = React.memo(
onUpdate={onUpdate}
onMove={onMove}
onTransfer={onTransfer}
onDuplicate={onDuplicate}
onDelete={onDelete}
onUserAdd={onUserAdd}
onUserRemove={onUserRemove}
@ -219,6 +222,7 @@ Card.propTypes = {
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
dueDate: PropTypes.instanceOf(Date),
isDueDateCompleted: PropTypes.bool,
stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types
coverUrl: PropTypes.string,
boardId: PropTypes.string.isRequired,
@ -238,6 +242,7 @@ Card.propTypes = {
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
@ -252,6 +257,7 @@ Card.propTypes = {
Card.defaultProps = {
dueDate: undefined,
isDueDateCompleted: undefined,
stopwatch: undefined,
coverUrl: undefined,
};

@ -21,6 +21,14 @@
background: #ebeef0;
color: #516b7a;
}
@media only screen and (max-width: 797px) {
&:focus,
&:active {
background: #ebeef0;
color: #516b7a;
}
}
}
.attachment {
@ -55,6 +63,12 @@
box-shadow: 0 1px 0 #ccc;
position: relative;
@media only screen and (max-width: 797px) {
.target {
opacity: 1;
}
}
&:hover {
background: #f5f6f7;
border-bottom-color: rgba(9, 30, 66, 0.25);

@ -5,6 +5,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useClosableForm, useField } from '../../hooks';
import { focusEnd } from '../../utils/element-helpers';
import styles from './NameEdit.module.scss';
@ -79,7 +80,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => {
if (isOpened) {
field.current.ref.current.focus();
focusEnd(field.current.ref.current);
}
}, [isOpened]);

@ -4,6 +4,8 @@ import classNames from 'classnames';
import { Progress } from 'semantic-ui-react';
import { useToggle } from '../../lib/hooks';
import Linkify from '../Linkify';
import styles from './Tasks.module.scss';
const Tasks = React.memo(({ items }) => {
@ -48,7 +50,7 @@ const Tasks = React.memo(({ items }) => {
key={item.id}
className={classNames(styles.task, item.isCompleted && styles.taskCompleted)}
>
{item.name}
<Linkify linkStopPropagation>{item.name}</Linkify>
</li>
))}
</ul>

@ -55,8 +55,10 @@
display: block;
font-size: 12px;
line-height: 14px;
overflow: hidden;
padding-bottom: 6px;
padding-left: 14px;
text-overflow: ellipsis;
&:before {
content: "";

@ -6,6 +6,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useForm } from '../../../hooks';
import { focusEnd } from '../../../utils/element-helpers';
import styles from './CommentEdit.module.scss';
@ -70,7 +71,7 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
useEffect(() => {
if (isOpened) {
textField.current.ref.current.focus();
focusEnd(textField.current.ref.current);
}
}, [isOpened]);

@ -4,6 +4,7 @@ import classNames from 'classnames';
import { useTranslation, Trans } from 'react-i18next';
import { Comment } from 'semantic-ui-react';
import getDateFormat from '../../../utils/get-date-format';
import { ActivityTypes } from '../../../constants/Enums';
import ItemComment from './ItemComment';
import User from '../../User';
@ -66,7 +67,7 @@ const Item = React.memo(({ type, data, createdAt, user }) => {
<div className={classNames(styles.content)}>
<div>{contentNode}</div>
<span className={styles.date}>
{t('format:longDateTime', {
{t(`format:${getDateFormat(createdAt)}`, {
postProcess: 'formatDate',
value: createdAt,
})}

@ -6,6 +6,7 @@ import { Comment } from 'semantic-ui-react';
import { usePopup } from '../../../lib/popup';
import { Markdown } from '../../../lib/custom-ui';
import getDateFormat from '../../../utils/get-date-format';
import CommentEdit from './CommentEdit';
import User from '../../User';
import DeleteStep from '../../DeleteStep';
@ -33,7 +34,7 @@ const ItemComment = React.memo(
<div className={styles.title}>
<span className={styles.author}>{user.name}</span>
<span className={styles.date}>
{t('format:longDateTime', {
{t(`format:${getDateFormat(createdAt)}`, {
postProcess: 'formatDate',
value: createdAt,
})}

@ -94,7 +94,7 @@ const AttachmentAddZone = React.memo(({ children, onCreate }) => {
return (
<>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...getRootProps()} className={styles.wrapper}>
<div {...getRootProps()}>
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
{children}
{/* eslint-disable-next-line react/jsx-props-no-spreading */}

@ -2,7 +2,7 @@
.dropzone {
background: white;
font-size: 20px;
font-weight: 700;
font-weight: bold;
height: 100%;
line-height: 30px;
opacity: 0.7;
@ -12,8 +12,4 @@
width: 100%;
z-index: 2001;
}
.wrapper {
overflow: hidden;
}
}

@ -16,7 +16,7 @@
.contentError {
color: #fff;
font-size: 20px;
font-weight: 700;
font-weight: bold;
height: 20px;
width: 470px;
}

@ -35,7 +35,7 @@
color: #5e6c84;
display: block;
font-size: 18px;
font-weight: 700;
font-weight: bold;
height: 100%;
line-height: 80px;
overflow: hidden;
@ -50,7 +50,7 @@
.name {
color: #17394d;
font-size: 14px;
font-weight: 700;
font-weight: bold;
line-height: 20px;
word-wrap: break-word;
}

@ -1,8 +1,8 @@
import React, { useCallback, useRef } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
import { Button, Checkbox, Grid, Icon, Modal } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup';
import { Markdown } from '../../lib/custom-ui';
@ -32,6 +32,7 @@ const CardModal = React.memo(
name,
description,
dueDate,
isDueDateCompleted,
stopwatch,
isSubscribed,
isActivitiesFetching,
@ -55,6 +56,7 @@ const CardModal = React.memo(
onUpdate,
onMove,
onTransfer,
onDuplicate,
onDelete,
onUserAdd,
onUserRemove,
@ -80,6 +82,7 @@ const CardModal = React.memo(
onClose,
}) => {
const [t] = useTranslation();
const [isLinkCopied, setIsLinkCopied] = useState(false);
const isGalleryOpened = useRef(false);
@ -116,6 +119,12 @@ const CardModal = React.memo(
[onUpdate],
);
const handleDueDateCompletionChange = useCallback(() => {
onUpdate({
isDueDateCompleted: !isDueDateCompleted,
});
}, [isDueDateCompleted, onUpdate]);
const handleStopwatchUpdate = useCallback(
(newStopwatch) => {
onUpdate({
@ -140,6 +149,19 @@ const CardModal = React.memo(
});
}, [isSubscribed, onUpdate]);
const handleDuplicateClick = useCallback(() => {
onDuplicate();
onClose();
}, [onDuplicate, onClose]);
const handleCopyLinkClick = useCallback(() => {
navigator.clipboard.writeText(window.location.href);
setIsLinkCopied(true);
setTimeout(() => {
setIsLinkCopied(false);
}, 5000);
}, []);
const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true;
}, []);
@ -217,6 +239,7 @@ const CardModal = React.memo(
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
@ -266,6 +289,7 @@ const CardModal = React.memo(
onMove={onLabelMove}
onDelete={onLabelDelete}
>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
@ -283,13 +307,24 @@ const CardModal = React.memo(
context: 'title',
})}
</div>
<span className={styles.attachment}>
<span className={classNames(styles.attachment, styles.attachmentDueDate)}>
{canEdit ? (
<>
<Checkbox
checked={isDueDateCompleted}
disabled={!canEdit}
onChange={handleDueDateCompletionChange}
/>
<DueDateEditPopup defaultValue={dueDate} onUpdate={handleDueDateUpdate}>
<DueDate value={dueDate} />
<DueDate
withStatusIcon
value={dueDate}
isCompleted={isDueDateCompleted}
/>
</DueDateEditPopup>
</>
) : (
<DueDate value={dueDate} />
<DueDate withStatusIcon value={dueDate} isCompleted={isDueDateCompleted} />
)}
</span>
</div>
@ -314,10 +349,11 @@ const CardModal = React.memo(
)}
</span>
{canEdit && (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button
onClick={handleToggleStopwatchClick}
type="button"
className={classNames(styles.attachment, styles.dueDate)}
onClick={handleToggleStopwatchClick}
>
<Icon
name={stopwatch.startedAt ? 'pause' : 'play'}
@ -493,6 +529,23 @@ const CardModal = React.memo(
{t('action.move')}
</Button>
</CardMovePopup>
<Button fluid className={styles.actionButton} onClick={handleDuplicateClick}>
<Icon name="copy outline" className={styles.actionIcon} />
{t('action.duplicate')}
</Button>
{window.isSecureContext && (
<Button fluid className={styles.actionButton} onClick={handleCopyLinkClick}>
<Icon
name={isLinkCopied ? 'linkify' : 'unlink'}
className={styles.actionIcon}
/>
{isLinkCopied
? t('common.linkIsCopied')
: t('action.copyLink', {
context: 'title',
})}
</Button>
)}
<DeletePopup
title="common.deleteCard"
content="common.areYouSureYouWantToDeleteThisCard"
@ -527,6 +580,7 @@ CardModal.propTypes = {
name: PropTypes.string.isRequired,
description: PropTypes.string,
dueDate: PropTypes.instanceOf(Date),
isDueDateCompleted: PropTypes.bool,
stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types
isSubscribed: PropTypes.bool.isRequired,
isActivitiesFetching: PropTypes.bool.isRequired,
@ -552,6 +606,7 @@ CardModal.propTypes = {
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onTransfer: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
@ -580,6 +635,7 @@ CardModal.propTypes = {
CardModal.defaultProps = {
description: undefined,
dueDate: undefined,
isDueDateCompleted: false,
stopwatch: undefined,
};

@ -20,18 +20,26 @@
.actionIcon {
color: #17394d;
display: inline;
margin-right: 8px;
}
.actions {
margin-bottom: 24px;
@media only screen and (max-width: 797px) {
flex: 1;
}
@media only screen and (max-width: 425px) {
padding: 0;
width: 100%;
}
}
.actionsTitle {
color: #8c8c8c;
font-size: 12px;
font-weight: 500;
font-weight: normal;
letter-spacing: 0.04em;
margin-top: 16px;
text-transform: uppercase;
@ -50,6 +58,12 @@
max-width: 100%;
}
.attachmentDueDate {
align-items: center;
display: flex;
gap: 4px;
}
.attachments {
display: inline-block;
margin: 0 8px 8px 0;
@ -63,6 +77,11 @@
.contentPadding {
padding: 8px 8px 0 16px;
@media only screen and (max-width: 797px) {
padding-right: 16px;
width: 100% !important;
}
}
.cursorPointer {
@ -139,7 +158,7 @@
.headerTitle {
color: #17394d;
font-size: 20px;
font-weight: 700;
font-weight: bold;
line-height: 24px;
}
@ -155,6 +174,11 @@
.modalPadding {
padding: 0px;
@media only screen and (max-width: 797px) {
display: flex;
flex-flow: column nowrap;
}
}
.moduleHeader {
@ -185,6 +209,15 @@
.sidebarPadding {
padding: 8px 16px 0 8px;
@media only screen and (max-width: 797px) {
align-items: flex-start;
display: flex;
flex-flow: row wrap;
gap: 10px;
padding-left: 16px;
width: 100% !important;
}
}
.text {

@ -57,6 +57,7 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
const mdEditorOptions = useMemo(
() => ({
autoDownloadFontAwesome: false,
autofocus: true,
spellChecker: false,
status: false,

@ -6,7 +6,7 @@
box-shadow: none;
color: #17394d;
font-size: 20px;
font-weight: 700;
font-weight: bold;
line-height: 24px;
margin: -5px;
overflow: hidden;

@ -8,6 +8,7 @@ import { usePopup } from '../../../lib/popup';
import NameEdit from './NameEdit';
import ActionsStep from './ActionsStep';
import Linkify from '../../Linkify';
import styles from './Item.module.scss';
@ -65,7 +66,7 @@ const Item = React.memo(
onClick={handleClick}
>
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name}
<Linkify linkStopPropagation>{name}</Linkify>
</span>
</span>
{isPersisted && canEdit && (

@ -5,6 +5,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { useField } from '../../../hooks';
import { focusEnd } from '../../../utils/element-helpers';
import styles from './NameEdit.module.scss';
@ -65,7 +66,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => {
if (isOpened) {
field.current.ref.current.focus();
focusEnd(field.current.ref.current);
}
}, [isOpened]);

@ -48,6 +48,8 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
return (
<>
{items.length > 0 && (
<>
<span className={styles.progressWrapper}>
<Progress
autoSuccess
value={completedItems.length}
@ -56,6 +58,11 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete
size="tiny"
className={styles.progress}
/>
</span>
<span className={styles.count}>
{completedItems.length}/{items.length}
</span>
</>
)}
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="tasks" type={DroppableTypes.TASK}>

@ -3,6 +3,23 @@
margin: 0 0 16px;
}
.progressWrapper {
display: inline-block;
padding: 3px 0;
vertical-align: top;
width: calc(100% - 50px);
}
.count {
color: #8c8c8c;
display: inline-block;
font-size: 14px;
line-height: 14px;
text-align: right;
vertical-align: top;
width: 50px;
}
.taskButton {
background: transparent;
border: none;

@ -4,6 +4,7 @@
border-radius: 4px;
bottom: 20px;
box-shadow: #b04632 0 1px 0;
max-width: calc(100% - 40px);
padding: 12px 18px;
position: fixed;
right: 20px;

@ -1,8 +1,12 @@
import upperFirst from 'lodash/upperFirst';
import React from 'react';
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { useForceUpdate } from '../../lib/hooks';
import getDateFormat from '../../utils/get-date-format';
import styles from './DueDate.module.scss';
@ -12,27 +16,111 @@ const SIZES = {
MEDIUM: 'medium',
};
const FORMATS = {
const STATUSES = {
DUE_SOON: 'dueSoon',
OVERDUE: 'overdue',
COMPLETED: 'completed',
};
const LONG_DATE_FORMAT_BY_SIZE = {
tiny: 'longDate',
small: 'longDate',
medium: 'longDateTime',
};
const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
const FULL_DATE_FORMAT_BY_SIZE = {
tiny: 'fullDate',
small: 'fullDate',
medium: 'fullDateTime',
};
const STATUS_ICON_PROPS_BY_STATUS = {
[STATUSES.DUE_SOON]: {
name: 'hourglass half',
color: 'orange',
},
[STATUSES.OVERDUE]: {
name: 'hourglass end',
color: 'red',
},
[STATUSES.COMPLETED]: {
name: 'checkmark',
color: 'green',
},
};
const getStatus = (dateTime, isCompleted) => {
if (isCompleted) {
return STATUSES.COMPLETED;
}
const secondsLeft = Math.floor((dateTime.getTime() - new Date().getTime()) / 1000);
if (secondsLeft <= 0) {
return STATUSES.OVERDUE;
}
if (secondsLeft <= 24 * 60 * 60) {
return STATUSES.DUE_SOON;
}
return null;
};
const DueDate = React.memo(({ value, size, isCompleted, isDisabled, withStatusIcon, onClick }) => {
const [t] = useTranslation();
const forceUpdate = useForceUpdate();
const statusRef = useRef(null);
statusRef.current = getStatus(value, isCompleted);
const intervalRef = useRef(null);
const dateFormat = getDateFormat(
value,
LONG_DATE_FORMAT_BY_SIZE[size],
FULL_DATE_FORMAT_BY_SIZE[size],
);
useEffect(() => {
if ([null, STATUSES.DUE_SOON].includes(statusRef.current)) {
intervalRef.current = setInterval(() => {
const status = getStatus(value, isCompleted);
if (status !== statusRef.current) {
forceUpdate();
}
if (status === STATUSES.OVERDUE) {
clearInterval(intervalRef.current);
}
}, 1000);
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [value, isCompleted, forceUpdate]);
const contentNode = (
<span
className={classNames(
styles.wrapper,
styles[`wrapper${upperFirst(size)}`],
!withStatusIcon && statusRef.current && styles[`wrapper${upperFirst(statusRef.current)}`],
onClick && styles.wrapperHoverable,
)}
>
{t(`format:${FORMATS[size]}`, {
{t(`format:${dateFormat}`, {
value,
postProcess: 'formatDate',
})}
{withStatusIcon && statusRef.current && (
// eslint-disable-next-line react/jsx-props-no-spreading
<Icon {...STATUS_ICON_PROPS_BY_STATUS[statusRef.current]} className={styles.statusIcon} />
)}
</span>
);
@ -48,14 +136,19 @@ const DueDate = React.memo(({ value, size, isDisabled, onClick }) => {
DueDate.propTypes = {
value: PropTypes.instanceOf(Date).isRequired,
size: PropTypes.oneOf(Object.values(SIZES)),
isCompleted: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool,
withStatusIcon: PropTypes.bool,
onClick: PropTypes.func,
onCompletionToggle: PropTypes.func,
};
DueDate.defaultProps = {
size: SIZES.MEDIUM,
isDisabled: false,
withStatusIcon: false,
onClick: undefined,
onCompletionToggle: undefined,
};
export default DueDate;

@ -8,16 +8,17 @@
padding: 0;
}
.statusIcon {
line-height: 1;
margin: 0 0 0 8px;
}
.wrapper {
background: #dce0e4;
border: none;
border-radius: 3px;
color: #6a808b;
display: inline-block;
outline: none;
text-align: left;
transition: background 0.3s ease;
vertical-align: top;
}
.wrapperHoverable:hover {
@ -43,4 +44,21 @@
line-height: 20px;
padding: 6px 12px;
}
/* Statuses */
.wrapperDueSoon {
background: #f2711c;
color: #fff;
}
.wrapperOverdue {
background: #db2828;
color: #fff;
}
.wrapperCompleted {
background: #21ba45;
color: #fff;
}
}

@ -1,5 +1,6 @@
:global(#app) {
.wrapper {
max-width: 100vw;
position: fixed;
width: 100%;
z-index: 1;

@ -3,11 +3,11 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Button, Icon, Menu } from 'semantic-ui-react';
import { useAuth } from 'react-oidc-context';
import { usePopup } from '../../lib/popup';
import Paths from '../../constants/Paths';
import NotificationsStep from './NotificationsStep';
import User from '../User';
import UserStep from '../UserStep';
import styles from './Header.module.scss';
@ -30,7 +30,6 @@ const Header = React.memo(
onUserSettingsClick,
onLogout,
}) => {
const auth = useAuth();
const handleProjectSettingsClick = useCallback(() => {
if (canEditProject) {
onProjectSettingsClick();
@ -40,11 +39,6 @@ const Header = React.memo(
const NotificationsPopup = usePopup(NotificationsStep, POPUP_PROPS);
const UserPopup = usePopup(UserStep, POPUP_PROPS);
const onFullLogout = () => {
auth.signoutSilent();
onLogout();
};
return (
<div className={styles.wrapper}>
{!project && (
@ -95,10 +89,11 @@ const Header = React.memo(
<UserPopup
isLogouting={isLogouting}
onSettingsClick={onUserSettingsClick}
onLogout={onFullLogout}
onLogout={onLogout}
>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
{user.name}
<span className={styles.userName}>{user.name}</span>
<User name={user.name} avatarUrl={user.avatarUrl} size="small" />
</Menu.Item>
</UserPopup>
</Menu.Menu>

@ -86,6 +86,15 @@
font-weight: bold;
}
.userName {
display: none;
margin-right: 10px;
@media only screen and (min-width: 797px) {
display: block;
}
}
.wrapper {
background: rgba(0, 0, 0, 0.24);
display: flex;

@ -49,7 +49,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 5px;

@ -17,7 +17,7 @@ const SIZES = {
const Label = React.memo(({ name, color, size, isDisabled, onClick }) => {
const contentNode = (
<div
<span
title={name}
className={classNames(
styles.wrapper,
@ -28,7 +28,7 @@ const Label = React.memo(({ name, color, size, isDisabled, onClick }) => {
)}
>
{name || '\u00A0'}
</div>
</span>
);
return onClick ? (

@ -11,13 +11,13 @@
.wrapper {
border-radius: 3px;
box-sizing: border-box;
color: #fff;
font-weight: 400;
outline: none;
display: inline-block;
font-weight: normal;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2);
vertical-align: top;
white-space: nowrap;
}

@ -25,7 +25,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 5px;

@ -0,0 +1,68 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import LinkifyReact from 'linkify-react';
import history from '../history';
const Linkify = React.memo(({ children, linkStopPropagation, ...props }) => {
const handleLinkClick = useCallback(
(event) => {
if (linkStopPropagation) {
event.stopPropagation();
}
if (!event.target.getAttribute('target')) {
event.preventDefault();
history.push(event.target.href);
}
},
[linkStopPropagation],
);
const linkRenderer = useCallback(
({ attributes: { href, ...linkProps }, content }) => {
let url;
try {
url = new URL(href, window.location);
} catch (error) {} // eslint-disable-line no-empty
const isSameSite = !!url && url.origin === window.location.origin;
return (
<a
{...linkProps} // eslint-disable-line react/jsx-props-no-spreading
href={href}
target={isSameSite ? undefined : '_blank'}
rel={isSameSite ? undefined : 'noreferrer'}
onClick={handleLinkClick}
>
{isSameSite ? url.pathname : content}
</a>
);
},
[handleLinkClick],
);
return (
<LinkifyReact
{...props} // eslint-disable-line react/jsx-props-no-spreading
options={{
defaultProtocol: 'https',
render: linkRenderer,
}}
>
{children}
</LinkifyReact>
);
});
Linkify.propTypes = {
children: PropTypes.string.isRequired,
linkStopPropagation: PropTypes.bool,
};
Linkify.defaultProps = {
linkStopPropagation: false,
};
export default Linkify;

@ -5,15 +5,17 @@ import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import ListSortStep from '../ListSortStep';
import DeleteStep from '../DeleteStep';
import styles from './ActionsStep.module.scss';
const StepTypes = {
DELETE: 'DELETE',
SORT: 'SORT',
};
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) => {
const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onSort, onDelete, onClose }) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
@ -27,11 +29,30 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
onClose();
}, [onCardAdd, onClose]);
const handleSortClick = useCallback(() => {
openStep(StepTypes.SORT);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step && step.type === StepTypes.DELETE) {
const handleSortTypeSelect = useCallback(
(type) => {
onSort({
type,
});
onClose();
},
[onSort, onClose],
);
if (step && step.type) {
switch (step.type) {
case StepTypes.SORT:
return <ListSortStep onTypeSelect={handleSortTypeSelect} onBack={handleBack} />;
case StepTypes.DELETE:
return (
<DeleteStep
title="common.deleteList"
@ -41,6 +62,8 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
onBack={handleBack}
/>
);
default:
}
}
return (
@ -62,6 +85,11 @@ const ActionsStep = React.memo(({ onNameEdit, onCardAdd, onDelete, onClose }) =>
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleSortClick}>
{t('action.sortList', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteList', {
context: 'title',
@ -77,6 +105,7 @@ ActionsStep.propTypes = {
onNameEdit: PropTypes.func.isRequired,
onCardAdd: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

@ -16,7 +16,18 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
import styles from './List.module.scss';
const List = React.memo(
({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => {
({
id,
index,
name,
isPersisted,
cardIds,
canEdit,
onUpdate,
onDelete,
onSort,
onCardCreate,
}) => {
const [t] = useTranslation();
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
@ -114,6 +125,7 @@ const List = React.memo(
onNameEdit={handleNameEdit}
onCardAdd={handleCardAdd}
onDelete={onDelete}
onSort={onSort}
>
<Button className={classNames(styles.headerButton, styles.target)}>
<Icon fitted name="pencil" size="small" />
@ -159,6 +171,7 @@ List.propTypes = {
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onSort: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onCardCreate: PropTypes.func.isRequired,
};

@ -43,7 +43,6 @@
max-height: calc(100vh - 268px);
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
width: 290px;
&:hover {
@ -81,6 +80,12 @@
&:hover .target {
opacity: 1;
}
@media only screen and (max-width: 797px) {
.target {
opacity: 1;
}
}
}
.headerEditable {
@ -104,6 +109,15 @@
background: rgba(9, 30, 66, 0.13);
color: #516b7a;
}
@media only screen and (max-width: 797px) {
&:focus,
&:active {
background: rgba(9, 30, 66, 0.13);
color: #516b7a;
}
}
}
.headerName {

@ -4,6 +4,7 @@ import TextareaAutosize from 'react-textarea-autosize';
import { TextArea } from 'semantic-ui-react';
import { useField } from '../../hooks';
import { focusEnd } from '../../utils/element-helpers';
import styles from './NameEdit.module.scss';
@ -71,7 +72,7 @@ const NameEdit = React.forwardRef(({ children, defaultValue, onUpdate }, ref) =>
useEffect(() => {
if (isOpened) {
field.current.ref.current.select();
focusEnd(field.current.ref.current);
}
}, [isOpened]);

@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { ListSortTypes } from '../../constants/Enums';
import styles from './ListSortStep.module.scss';
const ListSortStep = React.memo(({ onTypeSelect, onBack }) => {
const [t] = useTranslation();
return (
<>
<Popup.Header onBack={onBack}>
{t('common.sortList', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.NAME_ASC)}
>
{t('common.title')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.DUE_DATE_ASC)}
>
{t('common.dueDate')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_ASC)}
>
{t('common.oldestFirst')}
</Menu.Item>
<Menu.Item
className={styles.menuItem}
onClick={() => onTypeSelect(ListSortTypes.CREATED_AT_DESC)}
>
{t('common.newestFirst')}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
});
ListSortStep.propTypes = {
onTypeSelect: PropTypes.func.isRequired,
onBack: PropTypes.func,
};
ListSortStep.defaultProps = {
onBack: undefined,
};
export default ListSortStep;

@ -0,0 +1,11 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}

@ -0,0 +1,3 @@
import ListSortStep from './ListSortStep';
export default ListSortStep;

@ -1,10 +1,9 @@
import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useAuth } from 'react-oidc-context';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Form, Grid, Header, Message } from 'semantic-ui-react';
import { Button, Form, Grid, Header, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input } from '../../lib/custom-ui';
@ -19,6 +18,11 @@ const createMessage = (error) => {
}
switch (error.message) {
case 'Invalid credentials':
return {
type: 'error',
content: 'common.invalidCredentials',
};
case 'Invalid email or username':
return {
type: 'error',
@ -29,6 +33,21 @@ const createMessage = (error) => {
type: 'error',
content: 'common.invalidPassword',
};
case 'Use single sign-on':
return {
type: 'error',
content: 'common.useSingleSignOn',
};
case 'Email already in use':
return {
type: 'error',
content: 'common.emailAlreadyInUse',
};
case 'Username already in use':
return {
type: 'error',
content: 'common.usernameAlreadyInUse',
};
case 'Failed to fetch':
return {
type: 'warning',
@ -48,8 +67,17 @@ const createMessage = (error) => {
};
const Login = React.memo(
({ defaultData, isSubmitting, error, onAuthenticate, onMessageDismiss }) => {
const auth = useAuth();
({
defaultData,
isSubmitting,
isSubmittingUsingOidc,
error,
withOidc,
isOidcEnforced,
onAuthenticate,
onAuthenticateUsingOidc,
onMessageDismiss,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@ -85,12 +113,15 @@ const Login = React.memo(
}, [onAuthenticate, data]);
useEffect(() => {
if (!isOidcEnforced) {
emailOrUsernameField.current.focus();
}, []);
}
}, [isOidcEnforced]);
useEffect(() => {
if (wasSubmitting && !isSubmitting && error) {
switch (error.message) {
case 'Invalid credentials':
case 'Invalid email or username':
emailOrUsernameField.current.select();
@ -137,6 +168,7 @@ const Login = React.memo(
onDismiss={onMessageDismiss}
/>
)}
{!isOidcEnforced && (
<Form size="large" onSubmit={handleSubmit}>
<div className={styles.inputWrapper}>
<div className={styles.inputLabel}>{t('common.emailOrUsername')}</div>
@ -170,12 +202,24 @@ const Login = React.memo(
content={t('action.logIn')}
floated="right"
loading={isSubmitting}
disabled={isSubmitting}
disabled={isSubmitting || isSubmittingUsingOidc}
/>
</Form>
<Form.Button type="button" onClick={() => auth.signinRedirect()}>
Log in with SSO
</Form.Button>
)}
{withOidc && (
<Button
type="button"
fluid={isOidcEnforced}
primary={isOidcEnforced}
size={isOidcEnforced ? 'large' : undefined}
icon={isOidcEnforced ? 'right arrow' : undefined}
labelPosition={isOidcEnforced ? 'right' : undefined}
content={t('action.logInWithSSO')}
loading={isSubmittingUsingOidc}
disabled={isSubmitting || isSubmittingUsingOidc}
onClick={onAuthenticateUsingOidc}
/>
)}
</div>
</div>
</Grid.Column>
@ -206,10 +250,16 @@ const Login = React.memo(
);
Login.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
/* eslint-disable react/forbid-prop-types */
defaultData: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
isSubmitting: PropTypes.bool.isRequired,
isSubmittingUsingOidc: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
withOidc: PropTypes.bool.isRequired,
isOidcEnforced: PropTypes.bool.isRequired,
onAuthenticate: PropTypes.func.isRequired,
onAuthenticateUsingOidc: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
};

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loader } from 'semantic-ui-react';
import LoginContainer from '../containers/LoginContainer';
const LoginWrapper = React.memo(({ isInitializing }) => {
if (isInitializing) {
return <Loader active size="massive" />;
}
return <LoginContainer />;
});
LoginWrapper.propTypes = {
isInitializing: PropTypes.bool.isRequired,
};
export default LoginWrapper;

@ -3,6 +3,7 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Popup } from '../../lib/custom-ui';
import { useSteps } from '../../hooks';
import User from '../User';
@ -19,6 +20,7 @@ const ActionsStep = React.memo(
({
membership,
permissionsSelectStep,
title,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
@ -31,6 +33,7 @@ const ActionsStep = React.memo(
canLeave,
onUpdate,
onDelete,
onBack,
onClose,
}) => {
const [t] = useTranslation();
@ -53,6 +56,11 @@ const ActionsStep = React.memo(
[onUpdate],
);
const handleDeleteConfirm = useCallback(() => {
onDelete();
onClose();
}, [onDelete, onClose]);
if (step) {
switch (step.type) {
case StepTypes.EDIT_PERMISSIONS: {
@ -81,7 +89,7 @@ const ActionsStep = React.memo(
? leaveConfirmationButtonContent
: deleteConfirmationButtonContent
}
onConfirm={onDelete}
onConfirm={handleDeleteConfirm}
onBack={handleBack}
/>
);
@ -89,7 +97,7 @@ const ActionsStep = React.memo(
}
}
return (
const contentNode = (
<>
<span className={styles.user}>
<User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" />
@ -125,12 +133,26 @@ const ActionsStep = React.memo(
)}
</>
);
return onBack ? (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content>{contentNode}</Popup.Content>
</>
) : (
contentNode
);
},
);
ActionsStep.propTypes = {
membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
@ -143,11 +165,13 @@ ActionsStep.propTypes = {
canLeave: PropTypes.bool.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
ActionsStep.defaultProps = {
permissionsSelectStep: undefined,
title: 'common.memberActions',
leaveButtonContent: 'action.leaveBoard',
leaveConfirmationTitle: 'common.leaveBoard',
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
@ -157,6 +181,7 @@ ActionsStep.defaultProps = {
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
deleteConfirmationButtonContent: 'action.removeMember',
onUpdate: undefined,
onBack: undefined,
};
export default ActionsStep;

@ -4,7 +4,6 @@
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 5px;

@ -5,16 +5,21 @@ import { usePopup } from '../../lib/popup';
import AddStep from './AddStep';
import ActionsStep from './ActionsStep';
import MembershipsStep from './MembershipsStep';
import User from '../User';
import styles from './Memberships.module.scss';
const MAX_MEMBERS = 6;
const Memberships = React.memo(
({
items,
allUsers,
permissionsSelectStep,
title,
addTitle,
actionsTitle,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
@ -31,11 +36,14 @@ const Memberships = React.memo(
}) => {
const AddPopup = usePopup(AddStep);
const ActionsPopup = usePopup(ActionsStep);
const MembershipsPopup = usePopup(MembershipsStep);
const remainMembersCount = items.length - MAX_MEMBERS;
return (
<>
<span className={styles.users}>
{items.map((item) => (
{items.slice(0, MAX_MEMBERS).map((item) => (
<span key={item.id} className={styles.user}>
<ActionsPopup
membership={item}
@ -63,6 +71,30 @@ const Memberships = React.memo(
</span>
))}
</span>
{remainMembersCount > 0 && (
<MembershipsPopup
items={items}
permissionsSelectStep={permissionsSelectStep}
title={title}
actionsTitle={actionsTitle}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canEdit={canEdit}
canLeave={items.length > 1 || canLeaveIfLast}
onUpdate={onUpdate}
onDelete={onDelete}
>
<Button icon className={styles.addUser}>
+{remainMembersCount < 99 ? remainMembersCount : 99}
</Button>
</MembershipsPopup>
)}
{canEdit && (
<AddPopup
users={allUsers}
@ -85,7 +117,9 @@ Memberships.propTypes = {
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
addTitle: PropTypes.string,
actionsTitle: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
@ -103,7 +137,9 @@ Memberships.propTypes = {
Memberships.defaultProps = {
permissionsSelectStep: undefined,
title: undefined,
addTitle: undefined,
actionsTitle: undefined,
leaveButtonContent: undefined,
leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined,

@ -11,6 +11,10 @@
vertical-align: top;
width: 36px;
@media only screen and (max-width: 797px) {
margin-left: 10px;
}
&:hover {
background: rgba(0, 0, 0, 0.32);
}

@ -0,0 +1,121 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useSteps } from '../../hooks';
import ActionsStep from './ActionsStep';
import BoardMembershipsStep from '../BoardMembershipsStep';
const StepTypes = {
EDIT: 'EDIT',
};
const MembershipsStep = React.memo(
({
items,
permissionsSelectStep,
title,
actionsTitle,
leaveButtonContent,
leaveConfirmationTitle,
leaveConfirmationContent,
leaveConfirmationButtonContent,
deleteButtonContent,
deleteConfirmationTitle,
deleteConfirmationContent,
deleteConfirmationButtonContent,
canEdit,
canLeave,
onUpdate,
onDelete,
onClose,
}) => {
const [step, openStep, handleBack] = useSteps();
const handleUserSelect = useCallback(
(userId) => {
openStep(StepTypes.EDIT, {
userId,
});
},
[openStep],
);
if (step && step.type === StepTypes.EDIT) {
const currentItem = items.find((item) => item.userId === step.params.userId);
if (currentItem) {
return (
<ActionsStep
membership={currentItem}
permissionsSelectStep={permissionsSelectStep}
title={actionsTitle}
leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent}
leaveConfirmationButtonContent={leaveConfirmationButtonContent}
deleteButtonContent={deleteButtonContent}
deleteConfirmationTitle={deleteConfirmationTitle}
deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canEdit={canEdit}
canLeave={canLeave}
onUpdate={(data) => onUpdate(currentItem.id, data)}
onDelete={() => onDelete(currentItem.id)}
onBack={handleBack}
onClose={onClose}
/>
);
}
openStep(null);
}
return (
// FIXME: hack
<BoardMembershipsStep
items={items}
currentUserIds={[]}
title={title}
onUserSelect={handleUserSelect}
onUserDeselect={() => {}}
/>
);
},
);
MembershipsStep.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string,
actionsTitle: PropTypes.string,
leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string,
leaveConfirmationButtonContent: PropTypes.string,
deleteButtonContent: PropTypes.string,
deleteConfirmationTitle: PropTypes.string,
deleteConfirmationContent: PropTypes.string,
deleteConfirmationButtonContent: PropTypes.string,
canEdit: PropTypes.bool.isRequired,
canLeave: PropTypes.bool.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
MembershipsStep.defaultProps = {
permissionsSelectStep: undefined,
title: undefined,
actionsTitle: undefined,
leaveButtonContent: undefined,
leaveConfirmationTitle: undefined,
leaveConfirmationContent: undefined,
leaveConfirmationButtonContent: undefined,
deleteButtonContent: undefined,
deleteConfirmationTitle: undefined,
deleteConfirmationContent: undefined,
deleteConfirmationButtonContent: undefined,
onUpdate: undefined,
};
export default MembershipsStep;

@ -1,19 +0,0 @@
import { useAuth } from 'react-oidc-context';
import PropTypes from 'prop-types';
import React from 'react';
let isLoggingIn = true;
const OidcLogin = React.memo(({ onAuthenticate }) => {
const auth = useAuth();
if (isLoggingIn && auth.user) {
isLoggingIn = false;
const { user } = auth;
onAuthenticate(user);
}
});
OidcLogin.propTypes = {
onAuthenticate: PropTypes.func.isRequired,
};
export default OidcLogin;

@ -1,3 +0,0 @@
import OidcLogin from './OidcLogin';
export default OidcLogin;

@ -12,7 +12,9 @@ const ManagersPane = React.memo(({ items, allUsers, onCreate, onDelete }) => {
<Memberships
items={items}
allUsers={allUsers}
title="common.managers"
addTitle="common.addManager"
actionsTitle="common.managerActions"
leaveButtonContent="action.leaveProject"
leaveConfirmationTitle="common.leaveProject"
leaveConfirmationContent="common.areYouSureYouWantToLeaveProject"

@ -1,12 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { AuthProvider } from 'react-oidc-context';
import { Provider } from 'react-redux';
import { Route, Routes } from 'react-router-dom';
import { ReduxRouter } from '../lib/redux-router';
import Paths from '../constants/Paths';
import LoginContainer from '../containers/LoginContainer';
import LoginWrapperContainer from '../containers/LoginWrapperContainer';
import CoreContainer from '../containers/CoreContainer';
import NotFound from './NotFound';
@ -14,25 +13,16 @@ import 'react-datepicker/dist/react-datepicker.css';
import 'photoswipe/dist/photoswipe.css';
import 'easymde/dist/easymde.min.css';
import '../lib/custom-ui/styles.css';
import '../assets/css/font-awesome.css';
import '../styles.module.scss';
import OidcLoginContainer from '../containers/OidcLoginContainer';
function Root({ store, history, config }) {
function Root({ store, history }) {
return (
<AuthProvider
authority={config.authority}
client_id={config.clientId}
redirect_uri={config.redirectUri}
scope={config.scopes}
onSigninCallback={() => {
window.history.replaceState({}, document.title, window.location.pathname);
}}
>
<Provider store={store}>
<ReduxRouter history={history}>
<Routes>
<Route path={Paths.LOGIN} element={<LoginContainer />} />
<Route path={Paths.OIDC_LOGIN} element={<OidcLoginContainer />} />
<Route path={Paths.LOGIN} element={<LoginWrapperContainer />} />
<Route path={Paths.OIDC_CALLBACK} element={<LoginWrapperContainer />} />
<Route path={Paths.ROOT} element={<CoreContainer />} />
<Route path={Paths.PROJECTS} element={<CoreContainer />} />
<Route path={Paths.BOARDS} element={<CoreContainer />} />
@ -41,14 +31,13 @@ function Root({ store, history, config }) {
</Routes>
</ReduxRouter>
</Provider>
</AuthProvider>
);
}
Root.propTypes = {
/* eslint-disable react/forbid-prop-types */
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
};

@ -10,13 +10,10 @@
.wrapper {
background: #dce0e4;
border: none;
border-radius: 3px;
color: #6a808b;
display: inline-block;
font-variant-numeric: tabular-nums;
outline: none;
text-align: left;
transition: background 0.3s ease;
vertical-align: top;
}

@ -14,12 +14,10 @@
}
.wrapper {
border: none;
border-radius: 50%;
color: #fff;
display: inline-block;
line-height: 1;
outline: none;
text-align: center;
vertical-align: top;
}
@ -32,7 +30,6 @@
.wrapperTiny {
font-size: 10px;
font-weight: 400;
height: 24px;
line-height: 20px;
padding: 2px 0;
@ -41,7 +38,6 @@
.wrapperSmall {
font-size: 12px;
font-weight: 400;
height: 28px;
padding: 8px 0;
width: 28px;
@ -49,7 +45,6 @@
.wrapperMedium {
font-size: 14px;
font-weight: 500;
height: 32px;
padding: 10px 0;
width: 32px;
@ -57,7 +52,6 @@
.wrapperLarge {
font-size: 14px;
font-weight: 500;
height: 36px;
padding: 12px 0 10px;
width: 36px;
@ -65,7 +59,6 @@
.wrapperMassive {
font-size: 36px;
font-weight: 500;
height: 100px;
padding: 32px 0 10px;
width: 100px;

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

Loading…
Cancel
Save