├── server
├── test
│ ├── mocha.opts
│ ├── fixtures
│ │ └── .gitkeep
│ ├── integration
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ └── .gitkeep
│ │ └── models
│ │ │ └── User.test.js
│ └── lifecycle.test.js
├── views
│ └── .gitkeep
├── api
│ ├── helpers
│ │ ├── .gitkeep
│ │ ├── users
│ │ │ ├── get-admin-ids.js
│ │ │ ├── get-one-by-email-or-username.js
│ │ │ ├── get-board-memberships.js
│ │ │ ├── get-project-managers.js
│ │ │ ├── get-notifications.js
│ │ │ ├── is-board-member.js
│ │ │ ├── is-card-subscriber.js
│ │ │ ├── is-project-manager.js
│ │ │ ├── get-manager-project-ids.js
│ │ │ ├── get-membership-board-ids.js
│ │ │ ├── get-many.js
│ │ │ └── get-one.js
│ │ ├── utils
│ │ │ ├── jsonify-record.js
│ │ │ ├── clear-http-only-token-cookie.js
│ │ │ ├── verify-jwt-token.js
│ │ │ ├── map-records.js
│ │ │ ├── set-http-only-token-cookie.js
│ │ │ └── send-email.js
│ │ ├── cards
│ │ │ ├── get-many.js
│ │ │ ├── get-attachments.js
│ │ │ ├── get-card-labels.js
│ │ │ ├── get-card-memberships.js
│ │ │ ├── get-labels.js
│ │ │ ├── get-label-ids.js
│ │ │ ├── get-project-path.js
│ │ │ ├── get-subscription-user-ids.js
│ │ │ ├── get-tasks.js
│ │ │ └── get-card-subscriptions.js
│ │ ├── lists
│ │ │ ├── get-many.js
│ │ │ ├── get-project-path.js
│ │ │ └── get-cards.js
│ │ ├── projects
│ │ │ ├── get-many.js
│ │ │ ├── get-project-managers.js
│ │ │ ├── get-board-ids.js
│ │ │ ├── get-board-member-user-ids.js
│ │ │ ├── get-manager-user-ids.js
│ │ │ ├── get-manager-and-board-member-user-ids.js
│ │ │ └── get-boards.js
│ │ ├── tasks
│ │ │ ├── get-many.js
│ │ │ └── get-project-path.js
│ │ ├── attachments
│ │ │ ├── get-many.js
│ │ │ └── get-project-path.js
│ │ ├── boards
│ │ │ ├── get-many.js
│ │ │ ├── get-cards.js
│ │ │ ├── get-board-memberships.js
│ │ │ ├── get-card-ids.js
│ │ │ ├── get-member-user-ids.js
│ │ │ ├── get-project-path.js
│ │ │ ├── get-lists.js
│ │ │ └── get-labels.js
│ │ ├── card-labels
│ │ │ └── get-many.js
│ │ ├── labels
│ │ │ ├── get-many.js
│ │ │ └── get-project-path.js
│ │ ├── card-memberships
│ │ │ └── get-many.js
│ │ ├── notifications
│ │ │ └── get-many.js
│ │ ├── project-managers
│ │ │ └── get-many.js
│ │ ├── board-memberships
│ │ │ ├── get-many.js
│ │ │ └── get-project-path.js
│ │ ├── card-subscriptions
│ │ │ └── get-many.js
│ │ └── actions
│ │ │ ├── get-many.js
│ │ │ └── get-project-path.js
│ ├── hooks
│ │ └── .gitkeep
│ ├── models
│ │ └── .gitkeep
│ ├── policies
│ │ ├── .gitkeep
│ │ ├── is-admin.js
│ │ └── is-authenticated.js
│ ├── controllers
│ │ ├── .gitkeep
│ │ ├── users
│ │ │ └── index.js
│ │ ├── access-tokens
│ │ │ └── delete.js
│ │ └── notifications
│ │ │ └── update.js
│ └── responses
│ │ ├── .gitkeep
│ │ ├── conflict.js
│ │ ├── notFound.js
│ │ ├── forbidden.js
│ │ ├── unauthorized.js
│ │ └── unprocessableEntity.js
├── README.md
├── private
│ └── attachments
│ │ └── .gitkeep
├── public
│ ├── user-avatars
│ │ └── .gitkeep
│ └── project-background-images
│ │ └── .gitkeep
├── .eslintignore
├── config
│ ├── locales
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── es.json
│ │ └── fr.json
│ └── policies.js
├── db
│ ├── migrations
│ │ ├── 20221003140000_@.js
│ │ ├── 20230108213138_labels_reordering.js
│ │ ├── 20220713145452_add_position_to_task_table.js
│ │ ├── 20220729142434_add_index_on_type_to_action_table.js
│ │ ├── 20230227170557_rename_timer_to_stopwatch.js
│ │ ├── 20220725150723_add_language_to_user_account_table.js
│ │ ├── 20240831195806_additional_http_only_token_for_enhanced_security_in_browsers.js
│ │ ├── 20220803221221_add_password_changed_at_to_user_account_table.js
│ │ ├── 20240812065305_make_due_date_toggleable.js
│ │ ├── 20180721233450_create_project_table.js
│ │ ├── 20180722003437_create_label_table.js
│ │ ├── 20180722006570_create_task_table.js
│ │ ├── 20220815155645_add_permissions_to_board_membership_table.js
│ │ ├── 20221225224651_remove_board_types.js.js
│ │ ├── 20180722005928_create_card_label_table.js
│ │ ├── 20180722005359_create_card_membership_table.js
│ │ ├── 20180721234154_create_project_manager_table.js
│ │ ├── 20180722001747_create_board_membership_table.js
│ │ ├── 20180721021044_create_archive_table.js
│ │ ├── 20181024220134_create_action_table.js
│ │ ├── 20180722003502_create_list_table.js
│ │ ├── 20180722005122_create_card_subscription_table.js
│ │ ├── 20180722000627_create_board_table.js
│ │ ├── 20180722006688_create_attachment_table.js
│ │ ├── 20220906094517_create_session_table.js
│ │ └── 20181112104653_create_notification_table.js
│ └── init.js
├── .npmrc
└── .sailsrc
├── client
├── README.md
├── src
│ ├── version.js
│ ├── components
│ │ ├── Card
│ │ │ ├── index.js
│ │ │ ├── ActionsStep.module.scss
│ │ │ └── NameEdit.module.scss
│ │ ├── Core
│ │ │ ├── index.js
│ │ │ └── Core.module.scss
│ │ ├── List
│ │ │ ├── index.js
│ │ │ ├── ActionsStep.module.scss
│ │ │ ├── NameEdit.module.scss
│ │ │ └── CardAdd.module.scss
│ │ ├── User
│ │ │ └── index.js
│ │ ├── Board
│ │ │ ├── index.js
│ │ │ └── ListAdd.module.scss
│ │ ├── Fixed
│ │ │ ├── index.js
│ │ │ └── Fixed.module.scss
│ │ ├── Label
│ │ │ └── index.js
│ │ ├── Login
│ │ │ └── index.js
│ │ ├── Boards
│ │ │ ├── index.js
│ │ │ ├── AddStep
│ │ │ │ ├── index.js
│ │ │ │ └── ImportStep.module.scss
│ │ │ └── EditStep.module.scss
│ │ ├── CardModal
│ │ │ ├── Tasks
│ │ │ │ ├── index.js
│ │ │ │ ├── ActionsStep.module.scss
│ │ │ │ ├── Add.module.scss
│ │ │ │ └── NameEdit.module.scss
│ │ │ ├── index.js
│ │ │ ├── Activities
│ │ │ │ ├── index.js
│ │ │ │ ├── CommentAdd.module.scss
│ │ │ │ ├── CommentEdit.module.scss
│ │ │ │ └── Item.module.scss
│ │ │ ├── Attachments
│ │ │ │ ├── index.js
│ │ │ │ └── EditStep.module.scss
│ │ │ ├── AttachmentAddZone
│ │ │ │ ├── TextFileAddModal.module.scss
│ │ │ │ ├── index.js
│ │ │ │ └── AttachmentAddZone.module.scss
│ │ │ ├── AttachmentAddStep.module.scss
│ │ │ ├── DescriptionEdit.module.scss
│ │ │ └── NameField.module.scss
│ │ ├── DueDate
│ │ │ └── index.js
│ │ ├── Header
│ │ │ └── index.js
│ │ ├── Project
│ │ │ ├── index.js
│ │ │ ├── Project.module.scss
│ │ │ └── Project.jsx
│ │ ├── Static
│ │ │ └── index.js
│ │ ├── UsersModal
│ │ │ ├── Item
│ │ │ │ ├── index.js
│ │ │ │ ├── Item.module.scss
│ │ │ │ └── ActionsStep.module.scss
│ │ │ └── index.js
│ │ ├── Projects
│ │ │ └── index.js
│ │ ├── UserStep
│ │ │ ├── index.js
│ │ │ └── UserStep.module.scss
│ │ ├── Stopwatch
│ │ │ └── index.js
│ │ ├── Background
│ │ │ ├── index.js
│ │ │ └── Background.module.scss
│ │ ├── DeleteStep
│ │ │ ├── index.js
│ │ │ └── DeleteStep.module.scss
│ │ ├── LabelsStep
│ │ │ ├── index.js
│ │ │ ├── AddStep.module.scss
│ │ │ └── EditStep.module.scss
│ │ ├── Memberships
│ │ │ ├── AddStep
│ │ │ │ ├── index.js
│ │ │ │ └── AddStep.module.scss
│ │ │ ├── index.js
│ │ │ └── Memberships.module.scss
│ │ ├── BoardActions
│ │ │ ├── index.js
│ │ │ └── BoardActions.module.scss
│ │ ├── CardMoveStep
│ │ │ ├── index.js
│ │ │ └── CardMoveStep.module.scss
│ │ ├── ListSortStep
│ │ │ ├── index.js
│ │ │ └── ListSortStep.module.scss
│ │ ├── UserAddStep
│ │ │ ├── index.js
│ │ │ └── UserAddStep.module.scss
│ │ ├── DueDateEditStep
│ │ │ ├── index.js
│ │ │ └── DueDateEditStep.module.scss
│ │ ├── ProjectAddModal
│ │ │ ├── ProjectAddModal.module.scss
│ │ │ └── index.js
│ │ ├── UserSettingsModal
│ │ │ ├── AccountPane
│ │ │ │ ├── index.js
│ │ │ │ ├── AvatarEditStep.module.scss
│ │ │ │ └── AccountPane.module.scss
│ │ │ ├── index.js
│ │ │ ├── PreferencesPane.module.scss
│ │ │ ├── AboutPane.module.scss
│ │ │ └── AboutPane.jsx
│ │ ├── ProjectSettingsModal
│ │ │ ├── GeneralPane
│ │ │ │ ├── index.js
│ │ │ │ ├── InformationEdit.module.scss
│ │ │ │ └── GeneralPane.module.scss
│ │ │ ├── index.js
│ │ │ └── ManagersPane.module.scss
│ │ ├── StopwatchEditStep
│ │ │ ├── index.js
│ │ │ └── StopwatchEditStep.module.scss
│ │ ├── UserEmailEditStep
│ │ │ ├── index.js
│ │ │ └── UserEmailEditStep.module.scss
│ │ ├── UserInformationEdit
│ │ │ ├── index.js
│ │ │ └── UserInformationEdit.module.scss
│ │ ├── BoardMembershipsStep
│ │ │ ├── index.js
│ │ │ └── BoardMembershipsStep.module.scss
│ │ ├── UserPasswordEditStep
│ │ │ ├── index.js
│ │ │ └── UserPasswordEditStep.module.scss
│ │ ├── UserUsernameEditStep
│ │ │ ├── index.js
│ │ │ └── UserUsernameEditStep.module.scss
│ │ ├── BoardMembershipPermissionsSelectStep
│ │ │ ├── index.js
│ │ │ └── BoardMembershipPermissionsSelectStep.module.scss
│ │ ├── NotFound.jsx
│ │ └── LoginWrapper.jsx
│ ├── lib
│ │ ├── custom-ui
│ │ │ ├── components
│ │ │ │ ├── FilePicker
│ │ │ │ │ ├── FilePicker.module.css
│ │ │ │ │ └── index.js
│ │ │ │ ├── Input
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── InputPassword.module.css
│ │ │ │ │ ├── MaskedInput.jsx
│ │ │ │ │ ├── Input.jsx
│ │ │ │ │ └── InputMask.jsx
│ │ │ │ ├── Popup
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── Popup.jsx
│ │ │ │ │ ├── PopupHeader.module.css
│ │ │ │ │ └── PopupHeader.jsx
│ │ │ │ └── Markdown
│ │ │ │ │ └── index.js
│ │ │ ├── assets
│ │ │ │ ├── fonts
│ │ │ │ │ ├── icons.eot
│ │ │ │ │ ├── icons.otf
│ │ │ │ │ ├── icons.ttf
│ │ │ │ │ ├── icons.woff
│ │ │ │ │ ├── icons.woff2
│ │ │ │ │ ├── brand-icons.eot
│ │ │ │ │ ├── brand-icons.ttf
│ │ │ │ │ ├── brand-icons.woff
│ │ │ │ │ ├── brand-icons.woff2
│ │ │ │ │ ├── outline-icons.eot
│ │ │ │ │ ├── outline-icons.ttf
│ │ │ │ │ ├── Nunitoga-Bold.woff2
│ │ │ │ │ ├── outline-icons.woff
│ │ │ │ │ ├── outline-icons.woff2
│ │ │ │ │ ├── Nunitoga-Light.woff2
│ │ │ │ │ ├── Nunitoga-Medium.woff2
│ │ │ │ │ ├── Nunitoga-BoldItalic.woff2
│ │ │ │ │ ├── Nunitoga-LightItalic.woff2
│ │ │ │ │ └── Nunitoga-MediumItalic.woff2
│ │ │ │ └── images
│ │ │ │ │ └── flags.png
│ │ │ └── index.js
│ │ ├── hooks
│ │ │ ├── use-force-update.js
│ │ │ ├── index.js
│ │ │ ├── use-previous.js
│ │ │ ├── use-toggle.js
│ │ │ └── use-did-update.js
│ │ ├── popup
│ │ │ ├── close-popup.js
│ │ │ ├── index.js
│ │ │ └── Popup.module.css
│ │ └── redux-router
│ │ │ ├── index.js
│ │ │ ├── create-router-middleware.js
│ │ │ └── create-router-reducer.js
│ ├── history.js
│ ├── assets
│ │ ├── images
│ │ │ ├── cover.jpg
│ │ │ ├── logo.png
│ │ │ ├── plus-math-icon.svg
│ │ │ └── plus-icon.svg
│ │ └── fonts
│ │ │ ├── FontAwesome.otf
│ │ │ ├── fontawesome-webfont.eot
│ │ │ ├── fontawesome-webfont.ttf
│ │ │ ├── fontawesome-webfont.woff
│ │ │ └── fontawesome-webfont.woff2
│ ├── reducers
│ │ ├── orm.js
│ │ ├── router.js
│ │ ├── ui
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── socket.js
│ ├── sagas
│ │ ├── login
│ │ │ ├── watchers
│ │ │ │ ├── index.js
│ │ │ │ ├── router.js
│ │ │ │ └── login.js
│ │ │ ├── services
│ │ │ │ └── index.js
│ │ │ └── index.js
│ │ ├── core
│ │ │ ├── requests
│ │ │ │ └── index.js
│ │ │ ├── watchers
│ │ │ │ ├── router.js
│ │ │ │ ├── core.js
│ │ │ │ ├── modals.js
│ │ │ │ ├── notifications.js
│ │ │ │ └── comment-activities.js
│ │ │ └── services
│ │ │ │ └── modals.js
│ │ └── index.js
│ ├── utils
│ │ ├── local-id.js
│ │ ├── get-date-format.js
│ │ ├── local-id.test.js
│ │ ├── validator.js
│ │ ├── element-helpers.js
│ │ ├── match-paths.js
│ │ └── merge-records.js
│ ├── selectors
│ │ ├── modals.js
│ │ ├── socket.js
│ │ ├── attachments.js
│ │ ├── root.js
│ │ ├── tasks.js
│ │ ├── labels.js
│ │ ├── project-managers.js
│ │ └── board-memberships.js
│ ├── locales
│ │ ├── ja-JP
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── ko-KR
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── zh-CN
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── ar-YE
│ │ │ └── index.js
│ │ ├── cs-CZ
│ │ │ └── index.js
│ │ ├── da-DK
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── de-DE
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── en-GB
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── es-ES
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── fa-IR
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── fr-FR
│ │ │ └── index.js
│ │ ├── hu-HU
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── it-IT
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── pl-PL
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── ro-RO
│ │ │ └── index.js
│ │ ├── ru-RU
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── sv-SE
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── tr-TR
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── uz-UZ
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── zh-TW
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── bg-BG
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── nl-NL
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── pt-BR
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── sk-SK
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ ├── uk-UA
│ │ │ └── index.js
│ │ ├── id-ID
│ │ │ ├── index.js
│ │ │ └── login.js
│ │ └── en-US
│ │ │ ├── index.js
│ │ │ └── login.js
│ ├── api
│ │ ├── root.js
│ │ ├── card-labels.js
│ │ ├── card-memberships.js
│ │ ├── tasks.js
│ │ ├── labels.js
│ │ └── access-tokens.js
│ ├── models
│ │ ├── BaseModel.js
│ │ └── index.js
│ ├── constants
│ │ ├── ErrorCodes.js
│ │ ├── DroppableTypes.js
│ │ ├── ModalTypes.js
│ │ ├── Paths.js
│ │ ├── Enums.js
│ │ ├── LabelColors.js
│ │ └── ProjectBackgroundGradients.js
│ ├── setupTests.js
│ ├── entry-actions
│ │ ├── core.js
│ │ ├── socket.js
│ │ ├── login.js
│ │ ├── notifications.js
│ │ └── comment-activities.js
│ ├── hooks
│ │ ├── index.js
│ │ ├── use-field.js
│ │ ├── use-modal.js
│ │ ├── use-form.js
│ │ ├── use-steps.js
│ │ └── use-closable-form.js
│ ├── index.js
│ ├── actions
│ │ └── modals.js
│ ├── containers
│ │ ├── LoginWrapperContainer.js
│ │ ├── FixedContainer.js
│ │ ├── StaticContainer.js
│ │ ├── ProjectContainer.js
│ │ ├── ProjectAddModalContainer.js
│ │ ├── UserAddStepContainer.js
│ │ └── CoreContainer.js
│ └── orm.js
├── version-template.ejs
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ └── manifest.json
├── tests
│ └── acceptance
│ │ ├── config.js
│ │ ├── features
│ │ └── webUIDashboard
│ │ │ └── dashboard.feature
│ │ ├── pageObjects
│ │ └── DashboardPage.js
│ │ └── stepDefinitions
│ │ └── dashBoardContext.js
└── .gitignore
├── .husky
└── pre-commit
├── .gitattributes
├── demo.gif
├── .vscode
├── extensions.json
└── settings.json
├── start.sh
├── charts
└── planka
│ ├── Chart.lock
│ ├── templates
│ ├── serviceaccount.yaml
│ ├── tests
│ │ └── test-connection.yaml
│ ├── service.yaml
│ └── secret-oidc.yaml
│ └── .helmignore
├── config
└── development
│ ├── Dockerfile.server
│ └── Dockerfile.client
├── docker-compose-db.yml
├── .gitignore
├── SECURITY.md
├── healthcheck.js
├── .dockerignore
├── Dockerfile.base
└── .github
├── FUNDING.yml
└── workflows
└── lint.yml
/server/test/mocha.opts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/views/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/api/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/api/hooks/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/api/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/api/policies/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/api/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/api/responses/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/test/fixtures/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Planka client
2 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # Planka server
2 |
--------------------------------------------------------------------------------
/server/private/attachments/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/public/user-avatars/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/test/integration/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/public/project-background-images/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/test/integration/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | npx lint-staged
3 |
--------------------------------------------------------------------------------
/client/src/version.js:
--------------------------------------------------------------------------------
1 | export default '1.23.4';
2 |
--------------------------------------------------------------------------------
/client/version-template.ejs:
--------------------------------------------------------------------------------
1 | export default '<%= pkg.version %>';
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | client/src/lib/custom-ui/styles.css linguist-vendored
2 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/demo.gif
--------------------------------------------------------------------------------
/server/.eslintignore:
--------------------------------------------------------------------------------
1 | assets/dependencies/**/*.js
2 | views/**/*.ejs
3 | public/**/*.js
4 |
--------------------------------------------------------------------------------
/client/src/components/Card/index.js:
--------------------------------------------------------------------------------
1 | import Card from './Card';
2 |
3 | export default Card;
4 |
--------------------------------------------------------------------------------
/client/src/components/Core/index.js:
--------------------------------------------------------------------------------
1 | import Core from './Core';
2 |
3 | export default Core;
4 |
--------------------------------------------------------------------------------
/client/src/components/List/index.js:
--------------------------------------------------------------------------------
1 | import List from './List';
2 |
3 | export default List;
4 |
--------------------------------------------------------------------------------
/client/src/components/User/index.js:
--------------------------------------------------------------------------------
1 | import User from './User';
2 |
3 | export default User;
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/src/components/Board/index.js:
--------------------------------------------------------------------------------
1 | import Board from './Board';
2 |
3 | export default Board;
4 |
--------------------------------------------------------------------------------
/client/src/components/Fixed/index.js:
--------------------------------------------------------------------------------
1 | import Fixed from './Fixed';
2 |
3 | export default Fixed;
4 |
--------------------------------------------------------------------------------
/client/src/components/Label/index.js:
--------------------------------------------------------------------------------
1 | import Label from './Label';
2 |
3 | export default Label;
4 |
--------------------------------------------------------------------------------
/client/src/components/Login/index.js:
--------------------------------------------------------------------------------
1 | import Login from './Login';
2 |
3 | export default Login;
4 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/public/logo512.png
--------------------------------------------------------------------------------
/client/src/components/Boards/index.js:
--------------------------------------------------------------------------------
1 | import Boards from './Boards';
2 |
3 | export default Boards;
4 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Tasks/index.js:
--------------------------------------------------------------------------------
1 | import Task from './Tasks';
2 |
3 | export default Task;
4 |
--------------------------------------------------------------------------------
/client/src/components/DueDate/index.js:
--------------------------------------------------------------------------------
1 | import DueDate from './DueDate';
2 |
3 | export default DueDate;
4 |
--------------------------------------------------------------------------------
/client/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import Header from './Header';
2 |
3 | export default Header;
4 |
--------------------------------------------------------------------------------
/client/src/components/Project/index.js:
--------------------------------------------------------------------------------
1 | import Project from './Project';
2 |
3 | export default Project;
4 |
--------------------------------------------------------------------------------
/client/src/components/Static/index.js:
--------------------------------------------------------------------------------
1 | import Static from './Static';
2 |
3 | export default Static;
4 |
--------------------------------------------------------------------------------
/client/src/components/UsersModal/Item/index.js:
--------------------------------------------------------------------------------
1 | import Item from './Item';
2 |
3 | export default Item;
4 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/FilePicker/FilePicker.module.css:
--------------------------------------------------------------------------------
1 | .field {
2 | display: none;
3 | }
4 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export NODE_ENV=production && set -e && node db/init.js && node app.js --prod
3 |
--------------------------------------------------------------------------------
/client/src/components/Projects/index.js:
--------------------------------------------------------------------------------
1 | import Projects from './Projects';
2 |
3 | export default Projects;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserStep/index.js:
--------------------------------------------------------------------------------
1 | import UserStep from './UserStep';
2 |
3 | export default UserStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/Boards/AddStep/index.js:
--------------------------------------------------------------------------------
1 | import AddStep from './AddStep';
2 |
3 | export default AddStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/index.js:
--------------------------------------------------------------------------------
1 | import CardModal from './CardModal';
2 |
3 | export default CardModal;
4 |
--------------------------------------------------------------------------------
/client/src/components/Stopwatch/index.js:
--------------------------------------------------------------------------------
1 | import Stopwatch from './Stopwatch';
2 |
3 | export default Stopwatch;
4 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Input/index.js:
--------------------------------------------------------------------------------
1 | import Input from './Input';
2 |
3 | export default Input;
4 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Popup/index.js:
--------------------------------------------------------------------------------
1 | import Popup from './Popup';
2 |
3 | export default Popup;
4 |
--------------------------------------------------------------------------------
/server/config/locales/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "Willkommen",
3 | "A brand new app.": "Eine neue App."
4 | }
5 |
--------------------------------------------------------------------------------
/server/config/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "Welcome",
3 | "A brand new app.": "A brand new app."
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/components/Background/index.js:
--------------------------------------------------------------------------------
1 | import Background from './Background';
2 |
3 | export default Background;
4 |
--------------------------------------------------------------------------------
/client/src/components/DeleteStep/index.js:
--------------------------------------------------------------------------------
1 | import DeleteStep from './DeleteStep';
2 |
3 | export default DeleteStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/LabelsStep/index.js:
--------------------------------------------------------------------------------
1 | import LabelsStep from './LabelsStep';
2 |
3 | export default LabelsStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/Memberships/AddStep/index.js:
--------------------------------------------------------------------------------
1 | import AddStep from './AddStep';
2 |
3 | export default AddStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/UsersModal/index.js:
--------------------------------------------------------------------------------
1 | import UsersModal from './UsersModal';
2 |
3 | export default UsersModal;
4 |
--------------------------------------------------------------------------------
/client/src/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 |
3 | export default createBrowserHistory();
4 |
--------------------------------------------------------------------------------
/server/config/locales/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "Bienvenido",
3 | "A brand new app.": "Una nueva aplicación."
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/assets/images/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/assets/images/cover.jpg
--------------------------------------------------------------------------------
/client/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/assets/images/logo.png
--------------------------------------------------------------------------------
/client/src/components/BoardActions/index.js:
--------------------------------------------------------------------------------
1 | import BoardActions from './BoardActions';
2 |
3 | export default BoardActions;
4 |
--------------------------------------------------------------------------------
/client/src/components/CardMoveStep/index.js:
--------------------------------------------------------------------------------
1 | import CardMoveStep from './CardMoveStep';
2 |
3 | export default CardMoveStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/ListSortStep/index.js:
--------------------------------------------------------------------------------
1 | import ListSortStep from './ListSortStep';
2 |
3 | export default ListSortStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/Memberships/index.js:
--------------------------------------------------------------------------------
1 | import Memberships from './Memberships';
2 |
3 | export default Memberships;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserAddStep/index.js:
--------------------------------------------------------------------------------
1 | import UserAddStep from './UserAddStep';
2 |
3 | export default UserAddStep;
4 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Markdown/index.js:
--------------------------------------------------------------------------------
1 | import Markdown from './Markdown';
2 |
3 | export default Markdown;
4 |
--------------------------------------------------------------------------------
/client/src/lib/hooks/use-force-update.js:
--------------------------------------------------------------------------------
1 | import useToggle from './use-toggle';
2 |
3 | export default () => useToggle()[1];
4 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Activities/index.js:
--------------------------------------------------------------------------------
1 | import Activities from './Activities';
2 |
3 | export default Activities;
4 |
--------------------------------------------------------------------------------
/server/config/locales/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "Welcome": "Bienvenue",
3 | "A brand new app.": "Une toute nouvelle application."
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/assets/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/assets/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/client/src/components/CardModal/Attachments/index.js:
--------------------------------------------------------------------------------
1 | import Attachments from './Attachments';
2 |
3 | export default Attachments;
4 |
--------------------------------------------------------------------------------
/client/src/components/LabelsStep/AddStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .submitButton {
3 | margin-top: 12px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/FilePicker/index.js:
--------------------------------------------------------------------------------
1 | import FilePicker from './FilePicker';
2 |
3 | export default FilePicker;
4 |
--------------------------------------------------------------------------------
/client/src/components/DueDateEditStep/index.js:
--------------------------------------------------------------------------------
1 | import DueDateEditStep from './DueDateEditStep';
2 |
3 | export default DueDateEditStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/ProjectAddModal/ProjectAddModal.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 20px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/components/ProjectAddModal/index.js:
--------------------------------------------------------------------------------
1 | import ProjectAddModal from './ProjectAddModal';
2 |
3 | export default ProjectAddModal;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserSettingsModal/AccountPane/index.js:
--------------------------------------------------------------------------------
1 | import AccountPane from './AccountPane';
2 |
3 | export default AccountPane;
4 |
--------------------------------------------------------------------------------
/client/src/components/UsersModal/Item/Item.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .button {
3 | box-shadow: 0 1px 0 #cbcccc;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/lib/popup/close-popup.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | document.dispatchEvent(new MouseEvent('click')); // FIXME: hack
3 | };
4 |
--------------------------------------------------------------------------------
/client/src/components/ProjectSettingsModal/GeneralPane/index.js:
--------------------------------------------------------------------------------
1 | import GeneralPane from './GeneralPane';
2 |
3 | export default GeneralPane;
4 |
--------------------------------------------------------------------------------
/client/src/components/StopwatchEditStep/index.js:
--------------------------------------------------------------------------------
1 | import StopwatchEditStep from './StopwatchEditStep';
2 |
3 | export default StopwatchEditStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserEmailEditStep/index.js:
--------------------------------------------------------------------------------
1 | import UserEmailEditStep from './UserEmailEditStep';
2 |
3 | export default UserEmailEditStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserSettingsModal/index.js:
--------------------------------------------------------------------------------
1 | import UserSettingsModal from './UserSettingsModal';
2 |
3 | export default UserSettingsModal;
4 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Input/InputPassword.module.css:
--------------------------------------------------------------------------------
1 | .strengthBar {
2 | margin: 4px 0 0 !important;
3 | opacity: 0.5;
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/assets/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/assets/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/client/src/assets/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/assets/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/icons.eot
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/icons.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/icons.otf
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/icons.ttf
--------------------------------------------------------------------------------
/client/src/reducers/orm.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from 'redux-orm';
2 |
3 | import orm from '../orm';
4 |
5 | export default createReducer(orm);
6 |
--------------------------------------------------------------------------------
/client/src/sagas/login/watchers/index.js:
--------------------------------------------------------------------------------
1 | import router from './router';
2 | import login from './login';
3 |
4 | export default [router, login];
5 |
--------------------------------------------------------------------------------
/client/src/assets/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/assets/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/client/src/assets/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/assets/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/client/src/components/CardModal/AttachmentAddZone/TextFileAddModal.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 20px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/AttachmentAddZone/index.js:
--------------------------------------------------------------------------------
1 | import AttachmentAddZone from './AttachmentAddZone';
2 |
3 | export default AttachmentAddZone;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserInformationEdit/index.js:
--------------------------------------------------------------------------------
1 | import UserInformationEdit from './UserInformationEdit';
2 |
3 | export default UserInformationEdit;
4 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/icons.woff
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/icons.woff2
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/images/flags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/images/flags.png
--------------------------------------------------------------------------------
/client/src/lib/popup/index.js:
--------------------------------------------------------------------------------
1 | import usePopup from './use-popup';
2 | import closePopup from './close-popup';
3 |
4 | export { usePopup, closePopup };
5 |
--------------------------------------------------------------------------------
/client/src/utils/local-id.js:
--------------------------------------------------------------------------------
1 | export const createLocalId = () => `local:${Date.now()}`;
2 |
3 | export const isLocalId = (id) => id.startsWith('local:');
4 |
--------------------------------------------------------------------------------
/client/src/components/BoardMembershipsStep/index.js:
--------------------------------------------------------------------------------
1 | import BoardMembershipsStep from './BoardMembershipsStep';
2 |
3 | export default BoardMembershipsStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/ProjectSettingsModal/index.js:
--------------------------------------------------------------------------------
1 | import ProjectSettingsModal from './ProjectSettingsModal';
2 |
3 | export default ProjectSettingsModal;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserPasswordEditStep/index.js:
--------------------------------------------------------------------------------
1 | import UserPasswordEditStep from './UserPasswordEditStep';
2 |
3 | export default UserPasswordEditStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/UserUsernameEditStep/index.js:
--------------------------------------------------------------------------------
1 | import UserUsernameEditStep from './UserUsernameEditStep';
2 |
3 | export default UserUsernameEditStep;
4 |
--------------------------------------------------------------------------------
/client/src/components/ProjectSettingsModal/ManagersPane.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .wrapper {
3 | border: none;
4 | box-shadow: none;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/components/UserSettingsModal/PreferencesPane.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .wrapper {
3 | border: none;
4 | box-shadow: none;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/brand-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/brand-icons.eot
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/brand-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/brand-icons.ttf
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/brand-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/brand-icons.woff
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/brand-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/brand-icons.woff2
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/outline-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/outline-icons.eot
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/outline-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/outline-icons.ttf
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/Nunitoga-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/Nunitoga-Bold.woff2
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/outline-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/outline-icons.woff
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/outline-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/outline-icons.woff2
--------------------------------------------------------------------------------
/client/src/sagas/core/requests/index.js:
--------------------------------------------------------------------------------
1 | import core from './core';
2 | import boards from './boards';
3 |
4 | export default {
5 | ...core,
6 | ...boards,
7 | };
8 |
--------------------------------------------------------------------------------
/client/src/selectors/modals.js:
--------------------------------------------------------------------------------
1 | export const selectCurrentModal = ({ core: { currentModal } }) => currentModal;
2 |
3 | export default {
4 | selectCurrentModal,
5 | };
6 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/Nunitoga-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/Nunitoga-Light.woff2
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/Nunitoga-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/Nunitoga-Medium.woff2
--------------------------------------------------------------------------------
/client/src/sagas/login/services/index.js:
--------------------------------------------------------------------------------
1 | import router from './router';
2 | import login from './login';
3 |
4 | export default {
5 | ...router,
6 | ...login,
7 | };
8 |
--------------------------------------------------------------------------------
/client/src/components/DeleteStep/DeleteStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .content {
3 | color: #212121;
4 | padding-bottom: 6px;
5 | padding-left: 2px;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/components/Fixed/Fixed.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .wrapper {
3 | max-width: 100vw;
4 | position: fixed;
5 | width: 100%;
6 | z-index: 1;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/Nunitoga-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/Nunitoga-BoldItalic.woff2
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/Nunitoga-LightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/Nunitoga-LightItalic.woff2
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/assets/fonts/Nunitoga-MediumItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-planka-agpl/master/client/src/lib/custom-ui/assets/fonts/Nunitoga-MediumItalic.woff2
--------------------------------------------------------------------------------
/server/db/migrations/20221003140000_@.js:
--------------------------------------------------------------------------------
1 | /* Move to new naming by feature */
2 |
3 | module.exports.up = () => Promise.resolve();
4 | module.exports.down = () => Promise.resolve();
5 |
--------------------------------------------------------------------------------
/client/src/locales/ja-JP/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'ja-JP',
5 | country: 'jp',
6 | name: '日本語',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/ko-KR/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'ko-KR',
5 | country: 'kr',
6 | name: '한국어',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/zh-CN/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'zh-CN',
5 | country: 'cn',
6 | name: '中文',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/reducers/router.js:
--------------------------------------------------------------------------------
1 | import { createRouterReducer } from '../lib/redux-router';
2 |
3 | import history from '../history';
4 |
5 | export default createRouterReducer(history);
6 |
--------------------------------------------------------------------------------
/client/src/assets/images/plus-math-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/src/locales/ar-YE/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'ar-YE',
5 | country: 'ye',
6 | name: 'العربية',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/cs-CZ/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'cs-CZ',
5 | country: 'cz',
6 | name: 'Čeština',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/da-DK/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'da-DK',
5 | country: 'dk',
6 | name: 'Dansk',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/de-DE/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'de-DE',
5 | country: 'de',
6 | name: 'Deutsch',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/en-GB/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'en-GB',
5 | country: 'gb',
6 | name: 'English',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/es-ES/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'es-ES',
5 | country: 'es',
6 | name: 'Español',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/fa-IR/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'fa-IR',
5 | country: 'ir',
6 | name: 'فارسی',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/fr-FR/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'fr-FR',
5 | country: 'fr',
6 | name: 'Français',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/hu-HU/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'hu-HU',
5 | country: 'hu',
6 | name: 'Magyar',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/it-IT/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'it-IT',
5 | country: 'it',
6 | name: 'Italiano',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/pl-PL/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'pl-PL',
5 | country: 'pl',
6 | name: 'Polski',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/ro-RO/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'ro-RO',
5 | country: 'ro',
6 | name: 'Română',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/ru-RU/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'ru-RU',
5 | country: 'ru',
6 | name: 'Русский',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/sv-SE/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'sv-SE',
5 | country: 'se',
6 | name: 'Svenska',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/tr-TR/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'tr-TR',
5 | country: 'tr',
6 | name: 'Türkçe',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/uz-UZ/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'uz-UZ',
5 | country: 'uz',
6 | name: "O'zbek",
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/zh-TW/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'zh-TW',
5 | country: 'tw',
6 | name: '繁體中文',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/selectors/socket.js:
--------------------------------------------------------------------------------
1 | export const selectIsSocketDisconnected = ({ socket: { isDisconnected } }) => isDisconnected;
2 |
3 | export default {
4 | selectIsSocketDisconnected,
5 | };
6 |
--------------------------------------------------------------------------------
/client/src/locales/bg-BG/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'bg-BG',
5 | country: 'bg',
6 | name: 'Български',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/nl-NL/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'nl-NL',
5 | country: 'nl',
6 | name: 'Nederlands',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/pt-BR/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'pt-BR',
5 | country: 'br',
6 | name: 'Português',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/sk-SK/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'sk-SK',
5 | country: 'sk',
6 | name: 'Slovenčina',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/uk-UA/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'uk-UA',
5 | country: 'ua',
6 | name: 'Українська',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/locales/id-ID/index.js:
--------------------------------------------------------------------------------
1 | import login from './login';
2 |
3 | export default {
4 | language: 'id-ID',
5 | country: 'id',
6 | name: 'Bahasa Indonesia',
7 | embeddedLocale: login,
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/api/root.js:
--------------------------------------------------------------------------------
1 | import http from './http';
2 |
3 | /* Actions */
4 |
5 | const getConfig = (headers) => http.get('/config', undefined, headers);
6 |
7 | export default {
8 | getConfig,
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/components/LabelsStep/EditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .deleteButton {
3 | bottom: 12px;
4 | box-shadow: 0 1px 0 #cbcccc;
5 | position: absolute;
6 | right: 9px;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/server/api/controllers/users/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async fn() {
3 | const users = await sails.helpers.users.getMany();
4 |
5 | return {
6 | items: users,
7 | };
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/components/BoardMembershipPermissionsSelectStep/index.js:
--------------------------------------------------------------------------------
1 | import BoardMembershipPermissionsSelectStep from './BoardMembershipPermissionsSelectStep';
2 |
3 | export default BoardMembershipPermissionsSelectStep;
4 |
--------------------------------------------------------------------------------
/client/src/models/BaseModel.js:
--------------------------------------------------------------------------------
1 | import { Model } from 'redux-orm';
2 |
3 | export default class BaseModel extends Model {
4 | // eslint-disable-next-line no-underscore-dangle, class-methods-use-this
5 | _onDelete() {}
6 | }
7 |
--------------------------------------------------------------------------------
/server/api/policies/is-admin.js:
--------------------------------------------------------------------------------
1 | module.exports = async function isAuthenticated(req, res, proceed) {
2 | if (!req.currentUser.isAdmin) {
3 | return res.notFound(); // Forbidden
4 | }
5 |
6 | return proceed();
7 | };
8 |
--------------------------------------------------------------------------------
/client/src/constants/ErrorCodes.js:
--------------------------------------------------------------------------------
1 | const UNAUTHORIZED = 'E_UNAUTHORIZED';
2 | const NOT_FOUND = 'E_NOT_FOUND';
3 | const CONFLICT = 'E_CONFLICT';
4 |
5 | export default {
6 | UNAUTHORIZED,
7 | NOT_FOUND,
8 | CONFLICT,
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/components/Card/ActionsStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: -7px -12px -5px;
4 | width: calc(100% + 24px);
5 | }
6 |
7 | .menuItem {
8 | margin: 0;
9 | padding-left: 14px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/List/ActionsStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: -7px -12px -5px;
4 | width: calc(100% + 24px);
5 | }
6 |
7 | .menuItem {
8 | margin: 0;
9 | padding-left: 14px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/Project/Project.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .wrapper {
3 | background: rgba(0, 0, 0, 0.16);
4 | display: flex;
5 | flex-direction: column;
6 | height: 100%;
7 | padding: 10px 20px 0;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/components/UserSettingsModal/AboutPane.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .version {
3 | font-weight: bold;
4 | text-align: center;
5 | }
6 |
7 | .wrapper {
8 | border: none;
9 | box-shadow: none;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/UserStep/UserStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: -7px -12px -5px;
4 | width: calc(100% + 24px);
5 | }
6 |
7 | .menuItem {
8 | margin: 0;
9 | padding-left: 14px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/Background/Background.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .wrapper {
3 | height: 100%;
4 | max-height: 100vh;
5 | max-width: 100vw;
6 | position: fixed;
7 | width: 100%;
8 | z-index: -1;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-admin-ids.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async fn() {
3 | const users = await sails.helpers.users.getMany({
4 | isAdmin: true,
5 | });
6 |
7 | return sails.helpers.utils.mapRecords(users);
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Tasks/ActionsStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: -7px -12px -5px;
4 | width: calc(100% + 24px);
5 | }
6 |
7 | .menuItem {
8 | margin: 0;
9 | padding-left: 14px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/ListSortStep/ListSortStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: -7px -12px -5px;
4 | width: calc(100% + 24px);
5 | }
6 |
7 | .menuItem {
8 | margin: 0;
9 | padding-left: 14px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/UsersModal/Item/ActionsStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: -7px -12px -5px;
4 | width: calc(100% + 24px);
5 | }
6 |
7 | .menuItem {
8 | margin: 0;
9 | padding-left: 14px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/api/policies/is-authenticated.js:
--------------------------------------------------------------------------------
1 | module.exports = async function isAuthenticated(req, res, proceed) {
2 | if (!req.currentUser) {
3 | return res.unauthorized('Access token is missing, invalid or expired');
4 | }
5 |
6 | return proceed();
7 | };
8 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Popup/Popup.jsx:
--------------------------------------------------------------------------------
1 | import { Popup as SemanticUIPopup } from 'semantic-ui-react';
2 |
3 | import PopupHeader from './PopupHeader';
4 |
5 | export default class Popup extends SemanticUIPopup {
6 | static Header = PopupHeader;
7 | }
8 |
--------------------------------------------------------------------------------
/charts/planka/Chart.lock:
--------------------------------------------------------------------------------
1 | dependencies:
2 | - name: postgresql
3 | repository: https://charts.bitnami.com/bitnami
4 | version: 12.5.1
5 | digest: sha256:01dfb2d07ab6800b4a5a6c81f20f3377a758124b2b96b891d0cd6b4f64cf783b
6 | generated: "2023-05-15T00:54:48.1308917+01:00"
7 |
--------------------------------------------------------------------------------
/client/src/constants/DroppableTypes.js:
--------------------------------------------------------------------------------
1 | const BOARD = 'BOARD';
2 | const LABEL = 'LABEL';
3 | const LIST = 'LIST';
4 | const CARD = 'CARD';
5 | const TASK = 'TASK';
6 |
7 | export default {
8 | BOARD,
9 | LABEL,
10 | LIST,
11 | CARD,
12 | TASK,
13 | };
14 |
--------------------------------------------------------------------------------
/client/src/components/CardMoveStep/CardMoveStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 8px;
4 | }
5 |
6 | .text {
7 | color: #444444;
8 | font-size: 12px;
9 | font-weight: bold;
10 | padding-bottom: 6px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/components/UserAddStep/UserAddStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 8px;
4 | }
5 |
6 | .text {
7 | color: #444444;
8 | font-size: 12px;
9 | font-weight: bold;
10 | padding-bottom: 6px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/index.js:
--------------------------------------------------------------------------------
1 | import Input from './components/Input';
2 | import Popup from './components/Popup';
3 | import Markdown from './components/Markdown';
4 | import FilePicker from './components/FilePicker';
5 |
6 | export { Input, Popup, Markdown, FilePicker };
7 |
--------------------------------------------------------------------------------
/client/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/server/db/migrations/20230108213138_labels_reordering.js:
--------------------------------------------------------------------------------
1 | const { addPosition, removePosition } = require('../../utils/migrations');
2 |
3 | module.exports.up = (knex) => addPosition(knex, 'label', 'board_id');
4 |
5 | module.exports.down = (knex) => removePosition(knex, 'label');
6 |
--------------------------------------------------------------------------------
/client/src/components/UserEmailEditStep/UserEmailEditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 8px;
4 | }
5 |
6 | .text {
7 | color: #444444;
8 | font-size: 12px;
9 | font-weight: bold;
10 | padding-bottom: 6px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/lib/hooks/index.js:
--------------------------------------------------------------------------------
1 | import usePrevious from './use-previous';
2 | import useToggle from './use-toggle';
3 | import useForceUpdate from './use-force-update';
4 | import useDidUpdate from './use-did-update';
5 |
6 | export { usePrevious, useToggle, useForceUpdate, useDidUpdate };
7 |
--------------------------------------------------------------------------------
/client/src/lib/hooks/use-previous.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export default (value) => {
4 | const prevValue = useRef();
5 |
6 | useEffect(() => {
7 | prevValue.current = value;
8 | }, [value]);
9 |
10 | return prevValue.current;
11 | };
12 |
--------------------------------------------------------------------------------
/server/db/migrations/20220713145452_add_position_to_task_table.js:
--------------------------------------------------------------------------------
1 | const { addPosition, removePosition } = require('../../utils/migrations');
2 |
3 | module.exports.up = (knex) => addPosition(knex, 'task', 'card_id');
4 |
5 | module.exports.down = (knex) => removePosition(knex, 'task');
6 |
--------------------------------------------------------------------------------
/client/src/components/UserInformationEdit/UserInformationEdit.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 8px;
4 | }
5 |
6 | .text {
7 | color: #444444;
8 | font-size: 12px;
9 | font-weight: bold;
10 | padding-bottom: 6px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/components/UserPasswordEditStep/UserPasswordEditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 8px;
4 | }
5 |
6 | .text {
7 | color: #444444;
8 | font-size: 12px;
9 | font-weight: bold;
10 | padding-bottom: 6px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/components/UserUsernameEditStep/UserUsernameEditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 8px;
4 | }
5 |
6 | .text {
7 | color: #444444;
8 | font-size: 12px;
9 | font-weight: bold;
10 | padding-bottom: 6px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/locales/en-US/index.js:
--------------------------------------------------------------------------------
1 | import merge from 'lodash/merge';
2 |
3 | import login from './login';
4 | import core from './core';
5 |
6 | export default {
7 | language: 'en-US',
8 | country: 'us',
9 | name: 'English',
10 | embeddedLocale: merge(login, core),
11 | };
12 |
--------------------------------------------------------------------------------
/client/src/components/ProjectSettingsModal/GeneralPane/InformationEdit.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | margin-bottom: 8px;
4 | }
5 |
6 | .text {
7 | color: #444444;
8 | font-size: 12px;
9 | font-weight: bold;
10 | padding-bottom: 6px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/utils/get-date-format.js:
--------------------------------------------------------------------------------
1 | export default (value, longDateFormat = 'longDateTime', fullDateFormat = 'fullDateTime') => {
2 | const year = value.getFullYear();
3 | const currentYear = new Date().getFullYear();
4 |
5 | return year === currentYear ? longDateFormat : fullDateFormat;
6 | };
7 |
--------------------------------------------------------------------------------
/client/src/constants/ModalTypes.js:
--------------------------------------------------------------------------------
1 | const USERS = 'USERS';
2 | const USER_SETTINGS = 'USER_SETTINGS';
3 | const PROJECT_ADD = 'PROJECT_ADD';
4 | const PROJECT_SETTINGS = 'PROJECT_SETTINGS';
5 |
6 | export default {
7 | USERS,
8 | USER_SETTINGS,
9 | PROJECT_ADD,
10 | PROJECT_SETTINGS,
11 | };
12 |
--------------------------------------------------------------------------------
/client/src/entry-actions/core.js:
--------------------------------------------------------------------------------
1 | import EntryActionTypes from '../constants/EntryActionTypes';
2 |
3 | const logout = (invalidateAccessToken) => ({
4 | type: EntryActionTypes.LOGOUT,
5 | payload: {
6 | invalidateAccessToken,
7 | },
8 | });
9 |
10 | export default {
11 | logout,
12 | };
13 |
--------------------------------------------------------------------------------
/client/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | import useField from './use-field';
2 | import useForm from './use-form';
3 | import useSteps from './use-steps';
4 | import useModal from './use-modal';
5 | import useClosableForm from './use-closable-form';
6 |
7 | export { useField, useForm, useSteps, useModal, useClosableForm };
8 |
--------------------------------------------------------------------------------
/server/api/helpers/utils/jsonify-record.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | sync: true,
3 |
4 | inputs: {
5 | record: {
6 | type: 'ref',
7 | required: true,
8 | },
9 | },
10 |
11 | fn(inputs) {
12 | return inputs.record.toJSON ? inputs.record.toJSON() : inputs.record;
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Input/MaskedInput.jsx:
--------------------------------------------------------------------------------
1 | import InputMask from 'react-input-mask';
2 |
3 | export default class MaskedInput extends InputMask {
4 | focus(options) {
5 | this.getInputDOMNode().focus(options);
6 | }
7 |
8 | select() {
9 | this.getInputDOMNode().select();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/utils/local-id.test.js:
--------------------------------------------------------------------------------
1 | import { isLocalId } from './local-id';
2 |
3 | describe('isLocalId', () => {
4 | test('is valid', () => {
5 | expect(isLocalId('local:1234567890')).toBeTruthy();
6 | });
7 |
8 | test('is invalid', () => {
9 | expect(isLocalId('1234567890')).toBeFalsy();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/client/src/lib/hooks/use-toggle.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export default (defaultState = false) => {
4 | const [state, setState] = useState(defaultState);
5 |
6 | const toggle = useCallback(() => {
7 | setState((prevState) => !prevState);
8 | }, []);
9 |
10 | return [state, toggle];
11 | };
12 |
--------------------------------------------------------------------------------
/client/src/sagas/core/watchers/router.js:
--------------------------------------------------------------------------------
1 | import { takeEvery } from 'redux-saga/effects';
2 | import { LOCATION_CHANGE_HANDLE } from '../../../lib/redux-router';
3 |
4 | import services from '../services';
5 |
6 | export default function* routerWatchers() {
7 | yield takeEvery(LOCATION_CHANGE_HANDLE, () => services.handleLocationChange());
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/sagas/login/watchers/router.js:
--------------------------------------------------------------------------------
1 | import { takeEvery } from 'redux-saga/effects';
2 | import { LOCATION_CHANGE_HANDLE } from '../../../lib/redux-router';
3 |
4 | import services from '../services';
5 |
6 | export default function* routerWatchers() {
7 | yield takeEvery(LOCATION_CHANGE_HANDLE, () => services.handleLocationChange());
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/utils/validator.js:
--------------------------------------------------------------------------------
1 | import zxcvbn from 'zxcvbn';
2 |
3 | const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
4 |
5 | export const isPassword = (string) => zxcvbn(string).score >= 2; // TODO: move to config
6 |
7 | export const isUsername = (string) =>
8 | string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
9 |
--------------------------------------------------------------------------------
/server/.npmrc:
--------------------------------------------------------------------------------
1 | ######################
2 | # ╔╗╔╔═╗╔╦╗┬─┐┌─┐ #
3 | # ║║║╠═╝║║║├┬┘│ #
4 | # o╝╚╝╩ ╩ ╩┴└─└─┘ #
5 | ######################
6 |
7 | # Hide NPM log output unless it is related to an error of some kind:
8 | loglevel=error
9 |
10 | # Make "npm audit" an opt-in thing for subsequent installs within this app:
11 | audit=false
12 |
--------------------------------------------------------------------------------
/client/src/utils/element-helpers.js:
--------------------------------------------------------------------------------
1 | export const focusEnd = (element) => {
2 | element.focus();
3 | element.setSelectionRange(element.value.length + 1, element.value.length + 1);
4 | };
5 |
6 | export const isActiveTextElement = (element) =>
7 | ['input', 'textarea'].includes(element.tagName.toLowerCase()) &&
8 | element === document.activeElement;
9 |
--------------------------------------------------------------------------------
/server/db/migrations/20220729142434_add_index_on_type_to_action_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.table('action', (table) => {
3 | /* Indexes */
4 |
5 | table.index('type');
6 | });
7 |
8 | module.exports.down = (knex) =>
9 | knex.schema.table('action', (table) => {
10 | table.dropIndex('type');
11 | });
12 |
--------------------------------------------------------------------------------
/server/db/migrations/20230227170557_rename_timer_to_stopwatch.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.table('card', (table) => {
3 | table.renameColumn('timer', 'stopwatch');
4 | });
5 |
6 | module.exports.down = (knex) =>
7 | knex.schema.table('card', (table) => {
8 | table.renameColumn('stopwatch', 'timer');
9 | });
10 |
--------------------------------------------------------------------------------
/client/src/hooks/use-field.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export default (initialValue) => {
4 | const [value, setValue] = useState(initialValue);
5 |
6 | const handleChange = useCallback((_, { value: nextValue }) => {
7 | setValue(nextValue);
8 | }, []);
9 |
10 | return [value, handleChange, setValue];
11 | };
12 |
--------------------------------------------------------------------------------
/client/src/selectors/attachments.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'redux-orm';
2 |
3 | import orm from '../orm';
4 |
5 | export const selectIsAttachmentWithIdExists = createSelector(
6 | orm,
7 | (_, id) => id,
8 | ({ Attachment }, id) => Attachment.idExists(id),
9 | );
10 |
11 | export default {
12 | selectIsAttachmentWithIdExists,
13 | };
14 |
--------------------------------------------------------------------------------
/server/.sailsrc:
--------------------------------------------------------------------------------
1 | {
2 | "generators": {
3 | "modules": {}
4 | },
5 | "hooks": {
6 | "blueprints": false,
7 | "grunt": false,
8 | "i18n": false,
9 | "session": false
10 | },
11 | "paths": {
12 | "public": "public"
13 | },
14 | "_generatedWith": {
15 | "sails": "1.1.0",
16 | "sails-generate": "1.16.4"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/components/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTranslation } from 'react-i18next';
3 |
4 | function NotFound() {
5 | const [t] = useTranslation();
6 |
7 | return (
8 |
9 | {t('common.pageNotFound', {
10 | context: 'title',
11 | })}
12 |
13 | );
14 | }
15 |
16 | export default NotFound;
17 |
--------------------------------------------------------------------------------
/client/src/reducers/ui/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import authenticateForm from './authenticate-form';
4 | import userCreateForm from './user-create-form';
5 | import projectCreateForm from './project-create-form';
6 |
7 | export default combineReducers({
8 | authenticateForm,
9 | userCreateForm,
10 | projectCreateForm,
11 | });
12 |
--------------------------------------------------------------------------------
/config/development/Dockerfile.server:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine as server-dependencies
2 |
3 | RUN apk -U upgrade \
4 | && apk add build-base python3 \
5 | --no-cache
6 |
7 | WORKDIR /app
8 |
9 | COPY package.json package-lock.json ./
10 |
11 | RUN npm install npm@latest --global \
12 | && npm install pnpm --global \
13 | && pnpm import \
14 | && pnpm install
15 |
--------------------------------------------------------------------------------
/server/api/helpers/utils/clear-http-only-token-cookie.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | sync: true,
3 |
4 | inputs: {
5 | response: {
6 | type: 'ref',
7 | required: true,
8 | },
9 | },
10 |
11 | fn(inputs) {
12 | inputs.response.clearCookie('httpOnlyToken', {
13 | path: sails.config.custom.baseUrlPath,
14 | });
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/server/db/migrations/20220725150723_add_language_to_user_account_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.table('user_account', (table) => {
3 | /* Columns */
4 |
5 | table.text('language');
6 | });
7 |
8 | module.exports.down = (knex) =>
9 | knex.schema.table('user_account', (table) => {
10 | table.dropColumn('language');
11 | });
12 |
--------------------------------------------------------------------------------
/client/src/selectors/root.js:
--------------------------------------------------------------------------------
1 | export const selectIsInitializing = ({ root: { isInitializing } }) => isInitializing;
2 |
3 | export const selectConfig = ({ root: { config } }) => config;
4 |
5 | export const selectOidcConfig = (state) => selectConfig(state).oidc;
6 |
7 | export default {
8 | selectIsInitializing,
9 | selectConfig,
10 | selectOidcConfig,
11 | };
12 |
--------------------------------------------------------------------------------
/docker-compose-db.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | postgres:
5 | image: postgres:16-alpine
6 | restart: unless-stopped
7 | volumes:
8 | - db-data:/var/lib/postgresql/data
9 | ports:
10 | - 5432:5432
11 | environment:
12 | - POSTGRES_DB=planka
13 | - POSTGRES_HOST_AUTH_METHOD=trust
14 |
15 | volumes:
16 | db-data:
17 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return Card.find(inputs.criteria).sort('position');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/lists/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return List.find(inputs.criteria).sort('position');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/projects/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return Project.find(inputs.criteria).sort('id');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/tasks/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return Task.find(inputs.criteria).sort('position');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import store from './store';
5 | import history from './history';
6 | import Root from './components/Root';
7 |
8 | import './i18n';
9 |
10 | const root = ReactDOM.createRoot(document.getElementById('root'));
11 | root.render(React.createElement(Root, { store, history }));
12 |
--------------------------------------------------------------------------------
/server/api/helpers/attachments/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return Attachment.find(inputs.criteria).sort('id');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return Board.find(inputs.criteria).sort('position');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/card-labels/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return CardLabel.find(inputs.criteria).sort('id');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/labels/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return Label.find(inputs.criteria).sort('position');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/actions/modals.js:
--------------------------------------------------------------------------------
1 | import ActionTypes from '../constants/ActionTypes';
2 |
3 | const openModal = (type) => ({
4 | type: ActionTypes.MODAL_OPEN,
5 | payload: {
6 | type,
7 | },
8 | });
9 |
10 | const closeModal = () => ({
11 | type: ActionTypes.MODAL_CLOSE,
12 | payload: {},
13 | });
14 |
15 | export default {
16 | openModal,
17 | closeModal,
18 | };
19 |
--------------------------------------------------------------------------------
/client/src/sagas/core/services/modals.js:
--------------------------------------------------------------------------------
1 | import { put } from 'redux-saga/effects';
2 |
3 | import actions from '../../../actions';
4 |
5 | export function* openModal(type) {
6 | yield put(actions.openModal(type));
7 | }
8 |
9 | export function* closeModal() {
10 | yield put(actions.closeModal());
11 | }
12 |
13 | export default {
14 | openModal,
15 | closeModal,
16 | };
17 |
--------------------------------------------------------------------------------
/server/api/helpers/card-memberships/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return CardMembership.find(inputs.criteria).sort('id');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/notifications/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return Notification.find(inputs.criteria).sort('id DESC');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/project-managers/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return ProjectManager.find(inputs.criteria).sort('id');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/components/BoardMembershipPermissionsSelectStep/BoardMembershipPermissionsSelectStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: 0 auto 8px;
4 | width: 100%;
5 | }
6 |
7 | .menuItemDescription {
8 | opacity: 0.5;
9 | }
10 |
11 | .menuItemTitle {
12 | margin-bottom: 8px;
13 | }
14 |
15 | .settings {
16 | margin: 0 0 8px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/api/helpers/board-memberships/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return BoardMembership.find(inputs.criteria).sort('id');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/api/helpers/card-subscriptions/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | },
10 |
11 | async fn(inputs) {
12 | return CardSubscription.find(inputs.criteria).sort('id');
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/server/db/init.js:
--------------------------------------------------------------------------------
1 | const initKnex = require('knex');
2 |
3 | const knexfile = require('./knexfile');
4 |
5 | const knex = initKnex(knexfile);
6 |
7 | (async () => {
8 | try {
9 | await knex.migrate.latest();
10 | await knex.seed.run();
11 | } catch (error) {
12 | process.exitCode = 1;
13 |
14 | throw error;
15 | } finally {
16 | knex.destroy();
17 | }
18 | })();
19 |
--------------------------------------------------------------------------------
/charts/planka/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: {{ include "planka.serviceAccountName" . }}
6 | labels:
7 | {{- include "planka.labels" . | nindent 4 }}
8 | {{- with .Values.serviceAccount.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | {{- end }}
13 |
--------------------------------------------------------------------------------
/client/src/components/Boards/EditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .deleteButton {
3 | bottom: 12px;
4 | box-shadow: 0 1px 0 #cbcccc;
5 | position: absolute;
6 | right: 9px;
7 | }
8 |
9 | .field {
10 | margin-bottom: 8px;
11 | }
12 |
13 | .text {
14 | color: #444444;
15 | font-size: 12px;
16 | font-weight: bold;
17 | padding-bottom: 6px;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/lib/hooks/use-did-update.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export default (callback, dependencies) => {
4 | const isMounted = useRef(false);
5 |
6 | useEffect(() => {
7 | if (isMounted.current) {
8 | callback();
9 | } else {
10 | isMounted.current = true;
11 | }
12 | }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
13 | };
14 |
--------------------------------------------------------------------------------
/server/db/migrations/20240831195806_additional_http_only_token_for_enhanced_security_in_browsers.js:
--------------------------------------------------------------------------------
1 | module.exports.up = async (knex) =>
2 | knex.schema.table('session', (table) => {
3 | /* Columns */
4 |
5 | table.text('http_only_token');
6 | });
7 |
8 | module.exports.down = (knex) =>
9 | knex.schema.table('session', (table) => {
10 | table.dropColumn('http_only_token');
11 | });
12 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/AttachmentAddZone/AttachmentAddZone.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .dropzone {
3 | background: white;
4 | font-size: 20px;
5 | font-weight: bold;
6 | height: 100%;
7 | line-height: 30px;
8 | opacity: 0.7;
9 | padding: 200px 50px;
10 | position: absolute;
11 | text-align: center;
12 | width: 100%;
13 | z-index: 2001;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/db/migrations/20220803221221_add_password_changed_at_to_user_account_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.table('user_account', (table) => {
3 | /* Columns */
4 |
5 | table.timestamp('password_changed_at', true);
6 | });
7 |
8 | module.exports.down = (knex) =>
9 | knex.schema.table('user_account', (table) => {
10 | table.dropColumn('password_changed_at');
11 | });
12 |
--------------------------------------------------------------------------------
/client/src/lib/redux-router/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | LOCATION_CHANGE_HANDLE,
3 | HISTORY_METHOD_CALL,
4 | push,
5 | replace,
6 | go,
7 | back,
8 | forward,
9 | } from './actions';
10 | export { default as createRouterReducer } from './create-router-reducer';
11 | export { default as createRouterMiddleware } from './create-router-middleware';
12 | export { default as ReduxRouter } from './ReduxRouter';
13 |
--------------------------------------------------------------------------------
/client/tests/acceptance/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // environment
3 | adminUser: {
4 | email: 'demo@demo.demo',
5 | password: 'demo',
6 | },
7 | baseUrl: process.env.BASE_URL ?? 'http://localhost:1337/',
8 | // playwright
9 | slowMo: parseInt(process.env.SLOW_MO, 10) || 1000,
10 | timeout: parseInt(process.env.TIMEOUT, 10) || 6000,
11 | headless: process.env.HEADLESS !== 'true',
12 | };
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | docker-compose.override.yml
3 |
4 | .idea
5 | .DS_Store
6 |
7 | # Prevent another lockfile than package-lock.json (npm) from being created
8 | # If some case you are using pnpm or yarn, don't forget to generate npm lockfile
9 | # before commiting your code by running:
10 | # `npm i --package-lock-only`
11 | pnpm-lock.yaml
12 | yarn.lock
13 |
14 | # Chart dependencies
15 | **/charts/*.tgz
16 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Attachments/EditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .deleteButton {
3 | bottom: 12px;
4 | box-shadow: 0 1px 0 #cbcccc;
5 | position: absolute;
6 | right: 9px;
7 | }
8 |
9 | .field {
10 | margin-bottom: 8px;
11 | }
12 |
13 | .text {
14 | color: #444444;
15 | font-size: 12px;
16 | font-weight: bold;
17 | padding-bottom: 6px;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/hooks/use-modal.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export default (initialParams) => {
4 | const [modal, setModal] = useState(() => initialParams);
5 |
6 | const open = useCallback((params) => {
7 | setModal(params);
8 | }, []);
9 |
10 | const handleClose = useCallback(() => {
11 | setModal(null);
12 | }, []);
13 |
14 | return [modal, open, handleClose];
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/utils/match-paths.js:
--------------------------------------------------------------------------------
1 | import { matchPath } from 'react-router-dom';
2 |
3 | export default (pathname, paths) => {
4 | for (let i = 0; i < paths.length; i += 1) {
5 | const match = matchPath(
6 | {
7 | path: paths[i],
8 | end: true,
9 | },
10 | pathname,
11 | );
12 |
13 | if (match) {
14 | return match;
15 | }
16 | }
17 |
18 | return null;
19 | };
20 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/src/sagas/index.js:
--------------------------------------------------------------------------------
1 | import { call } from 'redux-saga/effects';
2 |
3 | import loginSaga from './login';
4 | import coreSaga from './core';
5 | import { getAccessToken } from '../utils/access-token-storage';
6 |
7 | export default function* rootSaga() {
8 | const accessToken = yield call(getAccessToken);
9 |
10 | if (!accessToken) {
11 | yield call(loginSaga);
12 | }
13 |
14 | yield call(coreSaga);
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/api/card-labels.js:
--------------------------------------------------------------------------------
1 | import socket from './socket';
2 |
3 | /* Actions */
4 |
5 | const createCardLabel = (cardId, data, headers) =>
6 | socket.post(`/cards/${cardId}/labels`, data, headers);
7 |
8 | const deleteCardLabel = (cardId, labelId, headers) =>
9 | socket.delete(`/cards/${cardId}/labels/${labelId}`, undefined, headers);
10 |
11 | export default {
12 | createCardLabel,
13 | deleteCardLabel,
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/components/Boards/AddStep/ImportStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .button {
3 | background: transparent;
4 | box-shadow: none;
5 | color: #6b808c;
6 | font-weight: normal;
7 | margin-top: 8px;
8 | padding: 6px 11px;
9 | text-align: left;
10 | transition: none;
11 |
12 | &:hover {
13 | background: rgba(9, 30, 66, 0.08);
14 | color: #092d42;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/containers/LoginWrapperContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import selectors from '../selectors';
4 | import LoginWrapper from '../components/LoginWrapper';
5 |
6 | const mapStateToProps = (state) => {
7 | const isInitializing = selectors.selectIsInitializing(state);
8 |
9 | return {
10 | isInitializing,
11 | };
12 | };
13 |
14 | export default connect(mapStateToProps)(LoginWrapper);
15 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Most recent release.
6 |
7 | ## Reporting a Vulnerability
8 |
9 | Please report any security issues you discovered to security@planka.cloud. If the issue is confirmed, we will release a patch as soon as possible depending on complexity.
10 |
11 | **Do NOT create public issues on GitHub for security vulnerabilities.**
12 |
13 | Thank you for your contribution!
14 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/AttachmentAddStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .divider {
3 | background: #eee;
4 | border: 0;
5 | height: 1px;
6 | margin-bottom: 8px;
7 | }
8 |
9 | .menu {
10 | margin: -7px -12px -5px;
11 | width: calc(100% + 24px);
12 | }
13 |
14 | .menuItem {
15 | margin: 0;
16 | padding-left: 14px;
17 | }
18 |
19 | .tip {
20 | opacity: 0.5;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/hooks/use-form.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | export default (initialData) => {
4 | const [data, setData] = useState(initialData);
5 |
6 | const handleFieldChange = useCallback((_, { name: fieldName, value }) => {
7 | setData((prevData) => ({
8 | ...prevData,
9 | [fieldName]: value,
10 | }));
11 | }, []);
12 |
13 | return [data, handleFieldChange, setData];
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import router from './router';
4 | import socket from './socket';
5 | import orm from './orm';
6 | import root from './root';
7 | import auth from './auth';
8 | import core from './core';
9 | import ui from './ui';
10 |
11 | export default combineReducers({
12 | router,
13 | socket,
14 | orm,
15 | root,
16 | auth,
17 | core,
18 | ui,
19 | });
20 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-cards.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.cards.getMany({
14 | boardId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/sagas/core/watchers/core.js:
--------------------------------------------------------------------------------
1 | import { all, takeEvery } from 'redux-saga/effects';
2 |
3 | import services from '../services';
4 | import EntryActionTypes from '../../../constants/EntryActionTypes';
5 |
6 | export default function* coreWatchers() {
7 | yield all([
8 | takeEvery(EntryActionTypes.LOGOUT, ({ payload: { invalidateAccessToken } }) =>
9 | services.logout(invalidateAccessToken),
10 | ),
11 | ]);
12 | }
13 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-attachments.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.attachments.getMany({
14 | cardId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-card-labels.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.cardLabels.getMany({
14 | cardId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/DescriptionEdit.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .controls {
3 | clear: both;
4 | margin-top: 6px;
5 | }
6 |
7 | .field {
8 | background: #fff;
9 | color: #17394d;
10 | display: block;
11 | font-size: 14px;
12 | line-height: 1.5;
13 | margin-bottom: 4px;
14 | overflow: hidden;
15 | resize: none;
16 |
17 | &:focus {
18 | outline: none;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/server/api/helpers/actions/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | limit: {
10 | type: 'number',
11 | },
12 | },
13 |
14 | async fn(inputs) {
15 | return Action.find(inputs.criteria).sort('id DESC').limit(inputs.limit);
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-one-by-email-or-username.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | emailOrUsername: {
4 | type: 'string',
5 | required: true,
6 | },
7 | },
8 |
9 | async fn(inputs) {
10 | const fieldName = inputs.emailOrUsername.includes('@') ? 'email' : 'username';
11 |
12 | return sails.helpers.users.getOne({
13 | [fieldName]: inputs.emailOrUsername.toLowerCase(),
14 | });
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/components/Memberships/AddStep/AddStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .users {
3 | margin-top: 8px;
4 | max-height: 60vh;
5 | overflow-x: hidden;
6 | overflow-y: auto;
7 |
8 | &::-webkit-scrollbar {
9 | width: 5px;
10 | }
11 |
12 | &::-webkit-scrollbar-track {
13 | background: transparent;
14 | }
15 |
16 | &::-webkit-scrollbar-thumb {
17 | border-radius: 3px;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/entry-actions/socket.js:
--------------------------------------------------------------------------------
1 | import EntryActionTypes from '../constants/EntryActionTypes';
2 |
3 | const handleSocketDisconnect = () => ({
4 | type: EntryActionTypes.SOCKET_DISCONNECT_HANDLE,
5 | payload: {},
6 | });
7 |
8 | const handleSocketReconnect = () => ({
9 | type: EntryActionTypes.SOCKET_RECONNECT_HANDLE,
10 | payload: {},
11 | });
12 |
13 | export default {
14 | handleSocketDisconnect,
15 | handleSocketReconnect,
16 | };
17 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-card-memberships.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.cardMemberships.getMany({
14 | cardId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-board-memberships.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.boardMemberships.getMany({
14 | userId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-project-managers.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.projectManagers.getMany({
14 | userId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-board-memberships.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.boardMemberships.getMany({
14 | boardId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/projects/get-project-managers.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.projectManagers.getMany({
14 | projectId: inputs.idOrIds,
15 | });
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/assets/images/plus-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/client/src/api/card-memberships.js:
--------------------------------------------------------------------------------
1 | import socket from './socket';
2 |
3 | /* Actions */
4 |
5 | const createCardMembership = (cardId, data, headers) =>
6 | socket.post(`/cards/${cardId}/memberships`, data, headers);
7 |
8 | const deleteCardMembership = (cardId, userId, headers) =>
9 | socket.delete(`/cards/${cardId}/memberships?userId=${userId}`, undefined, headers);
10 |
11 | export default {
12 | createCardMembership,
13 | deleteCardMembership,
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/api/tasks.js:
--------------------------------------------------------------------------------
1 | import socket from './socket';
2 |
3 | /* Actions */
4 |
5 | const createTask = (cardId, data, headers) => socket.post(`/cards/${cardId}/tasks`, data, headers);
6 |
7 | const updateTask = (id, data, headers) => socket.patch(`/tasks/${id}`, data, headers);
8 |
9 | const deleteTask = (id, headers) => socket.delete(`/tasks/${id}`, undefined, headers);
10 |
11 | export default {
12 | createTask,
13 | updateTask,
14 | deleteTask,
15 | };
16 |
--------------------------------------------------------------------------------
/charts/planka/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/client/src/sagas/core/watchers/modals.js:
--------------------------------------------------------------------------------
1 | import { all, takeEvery } from 'redux-saga/effects';
2 |
3 | import services from '../services';
4 | import EntryActionTypes from '../../../constants/EntryActionTypes';
5 |
6 | export default function* modalsWatchers() {
7 | yield all([
8 | takeEvery(EntryActionTypes.MODAL_OPEN, ({ payload: { type } }) => services.openModal(type)),
9 | takeEvery(EntryActionTypes.MODAL_CLOSE, () => services.closeModal()),
10 | ]);
11 | }
12 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-notifications.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | return sails.helpers.notifications.getMany({
14 | isRead: false,
15 | userId: inputs.idOrIds,
16 | });
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Input/Input.jsx:
--------------------------------------------------------------------------------
1 | import { Input as SemanticUIInput } from 'semantic-ui-react';
2 |
3 | import InputPassword from './InputPassword';
4 | import InputMask from './InputMask';
5 |
6 | export default class Input extends SemanticUIInput {
7 | static Password = InputPassword;
8 |
9 | static Mask = InputMask;
10 |
11 | focus = (options) => this.inputRef.current.focus(options);
12 |
13 | blur = () => this.inputRef.current.blur();
14 | }
15 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-card-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const cards = await sails.helpers.boards.getCards(inputs.idOrIds);
14 |
15 | return sails.helpers.utils.mapRecords(cards);
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/users/is-board-member.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | id: {
4 | type: 'string',
5 | required: true,
6 | },
7 | boardId: {
8 | type: 'string',
9 | required: true,
10 | },
11 | },
12 |
13 | async fn(inputs) {
14 | const boardMembership = await BoardMembership.findOne({
15 | boardId: inputs.boardId,
16 | userId: inputs.id,
17 | });
18 |
19 | return !!boardMembership;
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/charts/planka/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "planka.fullname" . }}-test-connection"
5 | labels:
6 | {{- include "planka.labels" . | nindent 4 }}
7 | annotations:
8 | "helm.sh/hook": test
9 | spec:
10 | containers:
11 | - name: wget
12 | image: busybox
13 | command: ['wget']
14 | args: ['{{ include "planka.fullname" . }}:{{ .Values.service.port }}']
15 | restartPolicy: Never
16 |
--------------------------------------------------------------------------------
/client/src/containers/FixedContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import selectors from '../selectors';
4 | import Fixed from '../components/Fixed';
5 |
6 | const mapStateToProps = (state) => {
7 | const { projectId } = selectors.selectPath(state);
8 | const currentBoard = selectors.selectCurrentBoard(state);
9 |
10 | return {
11 | projectId,
12 | board: currentBoard,
13 | };
14 | };
15 |
16 | export default connect(mapStateToProps)(Fixed);
17 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-labels.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const labelIds = await sails.helpers.cards.getLabelIds(inputs.idOrIds);
14 |
15 | return sails.helpers.labels.getMany(labelIds);
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/users/is-card-subscriber.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | id: {
4 | type: 'string',
5 | required: true,
6 | },
7 | cardId: {
8 | type: 'string',
9 | required: true,
10 | },
11 | },
12 |
13 | async fn(inputs) {
14 | const cardSubscription = await CardSubscription.findOne({
15 | cardId: inputs.cardId,
16 | userId: inputs.id,
17 | });
18 |
19 | return !!cardSubscription;
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/server/api/helpers/users/is-project-manager.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | id: {
4 | type: 'string',
5 | required: true,
6 | },
7 | projectId: {
8 | type: 'string',
9 | required: true,
10 | },
11 | },
12 |
13 | async fn(inputs) {
14 | const projectManager = await ProjectManager.findOne({
15 | projectId: inputs.projectId,
16 | userId: inputs.id,
17 | });
18 |
19 | return !!projectManager;
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/client/src/api/labels.js:
--------------------------------------------------------------------------------
1 | import socket from './socket';
2 |
3 | /* Actions */
4 |
5 | const createLabel = (boardId, data, headers) =>
6 | socket.post(`/boards/${boardId}/labels`, data, headers);
7 |
8 | const updateLabel = (id, data, headers) => socket.patch(`/labels/${id}`, data, headers);
9 |
10 | const deleteLabel = (id, headers) => socket.delete(`/labels/${id}`, undefined, headers);
11 |
12 | export default {
13 | createLabel,
14 | updateLabel,
15 | deleteLabel,
16 | };
17 |
--------------------------------------------------------------------------------
/server/api/helpers/projects/get-board-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const boards = await sails.helpers.projects.getBoards(inputs.idOrIds);
14 |
15 | return sails.helpers.utils.mapRecords(boards);
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/BoardMembershipsStep/BoardMembershipsStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .menu {
3 | margin: 8px auto 0;
4 | max-height: 60vh;
5 | overflow-x: hidden;
6 | overflow-y: auto;
7 | width: 100%;
8 |
9 | &::-webkit-scrollbar {
10 | width: 5px;
11 | }
12 |
13 | &::-webkit-scrollbar-track {
14 | background: transparent;
15 | }
16 |
17 | &::-webkit-scrollbar-thumb {
18 | border-radius: 3px;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/constants/Paths.js:
--------------------------------------------------------------------------------
1 | import Config from './Config';
2 |
3 | const ROOT = `${Config.BASE_PATH}/`;
4 | const LOGIN = `${Config.BASE_PATH}/login`;
5 | const OIDC_CALLBACK = `${Config.BASE_PATH}/oidc-callback`;
6 | const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
7 | const BOARDS = `${Config.BASE_PATH}/boards/:id`;
8 | const CARDS = `${Config.BASE_PATH}/cards/:id`;
9 |
10 | export default {
11 | ROOT,
12 | LOGIN,
13 | OIDC_CALLBACK,
14 | PROJECTS,
15 | BOARDS,
16 | CARDS,
17 | };
18 |
--------------------------------------------------------------------------------
/client/tests/acceptance/features/webUIDashboard/dashboard.feature:
--------------------------------------------------------------------------------
1 | Feature: dashboard
2 | As a admin
3 | I want to create a project
4 | So that I can manage project
5 |
6 | Scenario: create a new project
7 | Given user has browsed to the login page
8 | And user has logged in with email "demo@demo.demo" and password "demo"
9 | When the user creates a project with name "testproject" using the webUI
10 | Then the created project "testproject" should be opened
11 |
--------------------------------------------------------------------------------
/config/development/Dockerfile.client:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine as server-dependencies
2 |
3 | RUN apk -U upgrade \
4 | && apk add build-base python3 \
5 | --no-cache
6 |
7 | WORKDIR /app/client
8 | COPY package.json package-lock.json /app/client/
9 | RUN npm install npm@latest --global \
10 | && npm install pnpm --global \
11 | && pnpm import \
12 | && pnpm install
13 |
14 |
15 | WORKDIR /app/
16 | COPY ../../package.json ../../package-lock.json /app/
17 | RUN pnpm import \
18 | && pnpm install
19 |
--------------------------------------------------------------------------------
/server/test/integration/models/User.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('chai');
2 |
3 | describe('User (model)', () => {
4 | before(async () => {
5 | await User.create({
6 | email: 'test@test.test',
7 | password: 'test',
8 | name: 'test',
9 | });
10 | });
11 |
12 | describe('#find()', () => {
13 | it('should return 1 user', async () => {
14 | const users = await User.find();
15 |
16 | expect(users).to.have.lengthOf(1);
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/client/src/containers/StaticContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import selectors from '../selectors';
4 | import Static from '../components/Static';
5 |
6 | const mapStateToProps = (state) => {
7 | const { cardId, projectId } = selectors.selectPath(state);
8 | const currentBoard = selectors.selectCurrentBoard(state);
9 |
10 | return {
11 | projectId,
12 | cardId,
13 | board: currentBoard,
14 | };
15 | };
16 |
17 | export default connect(mapStateToProps)(Static);
18 |
--------------------------------------------------------------------------------
/client/src/containers/ProjectContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import selectors from '../selectors';
4 | import ModalTypes from '../constants/ModalTypes';
5 | import Project from '../components/Project';
6 |
7 | const mapStateToProps = (state) => {
8 | const currentModal = selectors.selectCurrentModal(state);
9 |
10 | return {
11 | isSettingsModalOpened: currentModal === ModalTypes.PROJECT_SETTINGS,
12 | };
13 | };
14 |
15 | export default connect(mapStateToProps)(Project);
16 |
--------------------------------------------------------------------------------
/server/api/helpers/projects/get-board-member-user-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const boardIds = await sails.helpers.projects.getBoardIds(inputs.idOrIds);
14 |
15 | return sails.helpers.boards.getMemberUserIds(boardIds);
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 2,
3 | "editor.formatOnSave": true,
4 | "files.insertFinalNewline": true,
5 | "files.trimFinalNewlines": true,
6 | "files.trimTrailingWhitespace": true,
7 | "eslint.format.enable": true,
8 | "eslint.workingDirectories": [
9 | "./client",
10 | "./server"
11 | ],
12 | "[javascript]": {
13 | "editor.defaultFormatter": "dbaeumer.vscode-eslint"
14 | },
15 | "[javascriptreact]": {
16 | "editor.defaultFormatter": "dbaeumer.vscode-eslint"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/db/migrations/20240812065305_make_due_date_toggleable.js:
--------------------------------------------------------------------------------
1 | module.exports.up = async (knex) => {
2 | await knex.schema.table('card', (table) => {
3 | /* Columns */
4 |
5 | table.boolean('is_due_date_completed');
6 | });
7 |
8 | return knex('card')
9 | .update({
10 | isDueDateCompleted: false,
11 | })
12 | .whereNotNull('due_date');
13 | };
14 |
15 | module.exports.down = (knex) =>
16 | knex.schema.table('card', (table) => {
17 | table.dropColumn('is_due_date_completed');
18 | });
19 |
--------------------------------------------------------------------------------
/client/src/lib/redux-router/create-router-middleware.js:
--------------------------------------------------------------------------------
1 | import { HISTORY_METHOD_CALL } from './actions';
2 |
3 | const createRouterMiddleware = (history) => {
4 | // eslint-disable-next-line consistent-return
5 | return () => (next) => (action) => {
6 | if (action.type !== HISTORY_METHOD_CALL) {
7 | return next(action);
8 | }
9 |
10 | const {
11 | payload: { method, args },
12 | } = action;
13 |
14 | history[method](...args);
15 | };
16 | };
17 |
18 | export default createRouterMiddleware;
19 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-label-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const cardLabels = await sails.helpers.cards.getCardLabels(inputs.idOrIds);
14 |
15 | return sails.helpers.utils.mapRecords(cardLabels, 'labelId', _.isArray(inputs.idOrIds));
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/test/lifecycle.test.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv');
2 | const sails = require('sails');
3 | const rc = require('sails/accessible/rc');
4 |
5 | process.env.NODE_ENV = 'test';
6 |
7 | before(function beforeCallback(done) {
8 | this.timeout(5000);
9 |
10 | dotenv.config();
11 |
12 | sails.lift(rc('sails'), (error) => {
13 | if (error) {
14 | return done(error);
15 | }
16 |
17 | return done();
18 | });
19 | });
20 |
21 | after(function afterCallback(done) {
22 | sails.lower(done);
23 | });
24 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Activities/CommentAdd.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .controls {
3 | clear: both;
4 | margin-top: 6px;
5 | }
6 |
7 | .field {
8 | background: #fff;
9 | border: 0;
10 | box-sizing: border-box;
11 | color: #333;
12 | display: block;
13 | line-height: 1.5;
14 | font-size: 14px;
15 | margin-bottom: 6px;
16 | overflow: hidden;
17 | padding: 8px 12px;
18 | resize: none;
19 | width: 100%;
20 |
21 | &:focus {
22 | outline: none;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/List/NameEdit.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | border: none;
4 | border-color: #bbb;
5 | border-radius: 3px;
6 | color: #17394d;
7 | display: block;
8 | font-weight: bold;
9 | line-height: 20px;
10 | margin: 0;
11 | outline: none;
12 | overflow: hidden;
13 | padding: 4px 8px;
14 | resize: none;
15 | width: 100%;
16 |
17 | &:focus {
18 | background: #fff;
19 | border-color: #5ba4cf;
20 | box-shadow: 0 0 0 1px #5ba4cf;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/components/LoginWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Loader } from 'semantic-ui-react';
4 |
5 | import LoginContainer from '../containers/LoginContainer';
6 |
7 | const LoginWrapper = React.memo(({ isInitializing }) => {
8 | if (isInitializing) {
9 | return ;
10 | }
11 |
12 | return ;
13 | });
14 |
15 | LoginWrapper.propTypes = {
16 | isInitializing: PropTypes.bool.isRequired,
17 | };
18 |
19 | export default LoginWrapper;
20 |
--------------------------------------------------------------------------------
/healthcheck.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const options = {
3 | host: 'localhost',
4 | port: 1337,
5 | timeout: 2000
6 | };
7 |
8 | const healthCheck = http.request(options, (res) => {
9 | console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
10 | if (res.statusCode == 200) {
11 | process.exit(0);
12 | }
13 | else {
14 | process.exit(1);
15 | }
16 | });
17 |
18 | healthCheck.on('error', function (err) {
19 | console.error('ERROR');
20 | process.exit(1);
21 | });
22 |
23 | healthCheck.end();
24 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-member-user-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const boardMemberships = await sails.helpers.boards.getBoardMemberships(inputs.idOrIds);
14 |
15 | return sails.helpers.utils.mapRecords(boardMemberships, 'userId', _.isArray(inputs.idOrIds));
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/projects/get-manager-user-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const projectManagers = await sails.helpers.projects.getProjectManagers(inputs.idOrIds);
14 |
15 | return sails.helpers.utils.mapRecords(projectManagers, 'userId', _.isArray(inputs.idOrIds));
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-manager-project-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const projectManagers = await sails.helpers.users.getProjectManagers(inputs.idOrIds);
14 |
15 | return sails.helpers.utils.mapRecords(projectManagers, 'projectId', _.isArray(inputs.idOrIds));
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-membership-board-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const boardMemberships = await sails.helpers.users.getBoardMemberships(inputs.idOrIds);
14 |
15 | return sails.helpers.utils.mapRecords(boardMemberships, 'boardId', _.isArray(inputs.idOrIds));
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/db/migrations/20180721233450_create_project_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('project', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.text('name').notNullable();
8 | table.jsonb('background');
9 | table.text('background_image_dirname');
10 |
11 | table.timestamp('created_at', true);
12 | table.timestamp('updated_at', true);
13 | });
14 |
15 | module.exports.down = (knex) => knex.schema.dropTable('project');
16 |
--------------------------------------------------------------------------------
/charts/planka/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "planka.fullname" . }}
5 | labels:
6 | {{- include "planka.labels" . | nindent 4 }}
7 | {{- with .Values.service.annotations }}
8 | annotations:
9 | {{- toYaml . | nindent 4 }}
10 | {{- end }}
11 | spec:
12 | type: {{ .Values.service.type }}
13 | ports:
14 | - port: {{ .Values.service.port }}
15 | targetPort: http
16 | protocol: TCP
17 | name: http
18 | selector:
19 | {{- include "planka.selectorLabels" . | nindent 4 }}
20 |
--------------------------------------------------------------------------------
/client/src/constants/Enums.js:
--------------------------------------------------------------------------------
1 | export const ProjectBackgroundTypes = {
2 | GRADIENT: 'gradient',
3 | IMAGE: 'image',
4 | };
5 |
6 | export const BoardMembershipRoles = {
7 | EDITOR: 'editor',
8 | VIEWER: 'viewer',
9 | };
10 |
11 | export const ListSortTypes = {
12 | NAME_ASC: 'name_asc',
13 | DUE_DATE_ASC: 'dueDate_asc',
14 | CREATED_AT_ASC: 'createdAt_asc',
15 | CREATED_AT_DESC: 'createdAt_desc',
16 | };
17 |
18 | export const ActivityTypes = {
19 | CREATE_CARD: 'createCard',
20 | MOVE_CARD: 'moveCard',
21 | COMMENT_CARD: 'commentCard',
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/selectors/tasks.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'redux-orm';
2 |
3 | import orm from '../orm';
4 |
5 | export const makeSelectTaskById = () =>
6 | createSelector(
7 | orm,
8 | (_, id) => id,
9 | ({ Task }, id) => {
10 | const taskModel = Task.withId(id);
11 |
12 | if (!taskModel) {
13 | return taskModel;
14 | }
15 |
16 | return taskModel.ref;
17 | },
18 | );
19 |
20 | export const selectTaskById = makeSelectTaskById();
21 |
22 | export default {
23 | makeSelectTaskById,
24 | selectTaskById,
25 | };
26 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Planka",
3 | "icons": [
4 | {
5 | "src": "favicon.ico",
6 | "sizes": "64x64 32x32 24x24 16x16",
7 | "type": "image/x-icon"
8 | },
9 | {
10 | "src": "logo192.png",
11 | "type": "image/png",
12 | "sizes": "192x192"
13 | },
14 | {
15 | "src": "logo512.png",
16 | "type": "image/png",
17 | "sizes": "512x512"
18 | }
19 | ],
20 | "start_url": ".",
21 | "display": "standalone",
22 | "theme_color": "#22252a",
23 | "background_color": "#ffffff"
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/components/BoardActions/BoardActions.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .action {
3 | align-items: center;
4 | display: flex;
5 | flex: 0 0 auto;
6 | }
7 |
8 | .actions {
9 | align-items: center;
10 | display: flex;
11 | gap: 20px;
12 | justify-content: flex-start;
13 | margin: 20px 20px;
14 | }
15 |
16 | .wrapper {
17 | overflow-x: auto;
18 | overflow-y: hidden;
19 | -ms-overflow-style: none;
20 | scrollbar-width: none;
21 |
22 | &::-webkit-scrollbar {
23 | display: none;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Tasks/Add.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .controls {
3 | clear: both;
4 | margin-top: 6px;
5 | }
6 |
7 | .field {
8 | background: #fff;
9 | border: 1px solid rgba(9, 30, 66, 0.08);
10 | border-radius: 3px;
11 | color: #17394d;
12 | display: block;
13 | line-height: 1.5;
14 | font-size: 14px;
15 | margin-bottom: 4px;
16 | overflow: hidden;
17 | padding: 8px 12px;
18 | resize: none;
19 | }
20 |
21 | .wrapper {
22 | margin-top: 6px;
23 | padding-bottom: 8px;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/DueDateEditStep/DueDateEditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .deleteButton {
3 | bottom: 12px;
4 | box-shadow: 0 1px 0 #cbcccc;
5 | position: absolute;
6 | right: 9px;
7 | }
8 |
9 | .fieldBox {
10 | display: inline-block;
11 | margin: 0 4px 12px;
12 | width: calc(50% - 8px);
13 | }
14 |
15 | .fieldWrapper {
16 | margin: 0 -4px;
17 | }
18 |
19 | .text {
20 | color: #444444;
21 | font-size: 12px;
22 | font-weight: bold;
23 | padding-bottom: 4px;
24 | padding-left: 2px;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | */README.md
2 | */.gitignore
3 | */node_modules
4 |
5 | **/.DS_Store
6 |
7 | server/**/.gitkeep
8 |
9 | server/.env
10 | server/.editorconfig
11 | server/.eslintignore
12 | server/.npmrc
13 | server/test
14 | server/logs
15 | server/.tmp
16 |
17 | server/views/*
18 |
19 | server/public/*
20 | !server/public/user-avatars
21 | server/public/user-avatars/*
22 | !server/public/project-background-images
23 | server/public/project-background-images/*
24 |
25 | server/private/*
26 | !server/private/attachments
27 | server/private/attachments/*
28 |
29 | client/build
30 |
--------------------------------------------------------------------------------
/client/src/selectors/labels.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'redux-orm';
2 |
3 | import orm from '../orm';
4 |
5 | export const makeSelectLabelById = () =>
6 | createSelector(
7 | orm,
8 | (_, id) => id,
9 | ({ Label }, id) => {
10 | const labelModel = Label.withId(id);
11 |
12 | if (!labelModel) {
13 | return labelModel;
14 | }
15 |
16 | return labelModel.ref;
17 | },
18 | );
19 |
20 | export const selectLabelById = makeSelectLabelById();
21 |
22 | export default {
23 | makeSelectLabelById,
24 | selectLabelById,
25 | };
26 |
--------------------------------------------------------------------------------
/client/src/components/Card/NameEdit.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | border: none;
4 | margin-bottom: 4px;
5 | outline: none;
6 | overflow: hidden;
7 | padding: 0;
8 | resize: none;
9 | width: 100%;
10 | word-wrap: break-word;
11 | }
12 |
13 | .fieldWrapper {
14 | background: #fff;
15 | border-radius: 3px;
16 | box-shadow: 0 1px 0 #ccc;
17 | margin-bottom: 8px;
18 | min-height: 20px;
19 | padding: 6px 8px 2px;
20 | }
21 |
22 | .submitButton {
23 | margin-bottom: 8px;
24 | vertical-align: top;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/constants/LabelColors.js:
--------------------------------------------------------------------------------
1 | export default [
2 | 'berry-red',
3 | 'pumpkin-orange',
4 | 'lagoon-blue',
5 | 'pink-tulip',
6 | 'light-mud',
7 | 'orange-peel',
8 | 'bright-moss',
9 | 'antique-blue',
10 | 'dark-granite',
11 | 'lagune-blue',
12 | 'sunny-grass',
13 | 'morning-sky',
14 | 'light-orange',
15 | 'midnight-blue',
16 | 'tank-green',
17 | 'gun-metal',
18 | 'wet-moss',
19 | 'red-burgundy',
20 | 'light-concrete',
21 | 'apricot-red',
22 | 'desert-sand',
23 | 'navy-blue',
24 | 'egg-yellow',
25 | 'coral-green',
26 | 'light-cocoa',
27 | ];
28 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/NameField.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | background: transparent;
4 | border: 1px solid transparent;
5 | border-radius: 3px;
6 | box-shadow: none;
7 | color: #17394d;
8 | font-size: 20px;
9 | font-weight: bold;
10 | line-height: 24px;
11 | margin: -5px;
12 | overflow: hidden;
13 | padding: 4px;
14 | resize: none;
15 | width: 100%;
16 |
17 | &:focus {
18 | background: #fff;
19 | border-color: #5ba4cf;
20 | box-shadow: 0 0 2px 0 #5ba4cf;
21 | outline: 0;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/constants/ProjectBackgroundGradients.js:
--------------------------------------------------------------------------------
1 | export default [
2 | 'old-lime',
3 | 'ocean-dive',
4 | 'tzepesch-style',
5 | 'jungle-mesh',
6 | 'strawberry-dust',
7 | 'purple-rose',
8 | 'sun-scream',
9 | 'warm-rust',
10 | 'sky-change',
11 | 'green-eyes',
12 | 'blue-xchange',
13 | 'blood-orange',
14 | 'sour-peel',
15 | 'green-ninja',
16 | 'algae-green',
17 | 'coral-reef',
18 | 'steel-grey',
19 | 'heat-waves',
20 | 'velvet-lounge',
21 | 'purple-rain',
22 | 'blue-steel',
23 | 'blueish-curve',
24 | 'prism-light',
25 | 'green-mist',
26 | 'red-curtain',
27 | ];
28 |
--------------------------------------------------------------------------------
/client/src/sagas/login/index.js:
--------------------------------------------------------------------------------
1 | import { all, call, cancel, fork, take } from 'redux-saga/effects';
2 |
3 | import watchers from './watchers';
4 | import services from './services';
5 | import ActionTypes from '../../constants/ActionTypes';
6 |
7 | export default function* loginSaga() {
8 | const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
9 |
10 | yield fork(services.initializeLogin);
11 |
12 | yield take([ActionTypes.AUTHENTICATE__SUCCESS, ActionTypes.USING_OIDC_AUTHENTICATE__SUCCESS]);
13 |
14 | yield cancel(watcherTasks);
15 | yield call(services.goToRoot);
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/api/access-tokens.js:
--------------------------------------------------------------------------------
1 | import http from './http';
2 |
3 | /* Actions */
4 |
5 | const createAccessToken = (data, headers) =>
6 | http.post('/access-tokens?withHttpOnlyToken=true', data, headers);
7 |
8 | const exchangeForAccessTokenUsingOidc = (data, headers) =>
9 | http.post('/access-tokens/exchange-using-oidc?withHttpOnlyToken=true', data, headers);
10 |
11 | const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', undefined, headers);
12 |
13 | export default {
14 | createAccessToken,
15 | exchangeForAccessTokenUsingOidc,
16 | deleteCurrentAccessToken,
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Activities/CommentEdit.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .controls {
3 | clear: both;
4 | margin-top: 6px;
5 | }
6 |
7 | .field {
8 | background: #fff;
9 | border: 1px solid rgba(9, 30, 66, 0.13);
10 | border-radius: 3px;
11 | box-sizing: border-box;
12 | color: #333;
13 | display: block;
14 | line-height: 1.4;
15 | font-size: 14px;
16 | margin-bottom: 4px;
17 | overflow: hidden;
18 | padding: 8px 12px;
19 | resize: none;
20 | width: 100%;
21 |
22 | &:focus {
23 | outline: none;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/entry-actions/login.js:
--------------------------------------------------------------------------------
1 | import EntryActionTypes from '../constants/EntryActionTypes';
2 |
3 | const authenticate = (data) => ({
4 | type: EntryActionTypes.AUTHENTICATE,
5 | payload: {
6 | data,
7 | },
8 | });
9 |
10 | const authenticateUsingOidc = () => ({
11 | type: EntryActionTypes.USING_OIDC_AUTHENTICATE,
12 | payload: {},
13 | });
14 |
15 | const clearAuthenticateError = () => ({
16 | type: EntryActionTypes.AUTHENTICATE_ERROR_CLEAR,
17 | payload: {},
18 | });
19 |
20 | export default {
21 | authenticate,
22 | authenticateUsingOidc,
23 | clearAuthenticateError,
24 | };
25 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722003437_create_label_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('label', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('board_id').notNullable();
8 |
9 | table.text('name');
10 | table.text('color').notNullable();
11 |
12 | table.timestamp('created_at', true);
13 | table.timestamp('updated_at', true);
14 |
15 | /* Indexes */
16 |
17 | table.index('board_id');
18 | });
19 |
20 | module.exports.down = (knex) => knex.schema.dropTable('label');
21 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Tasks/NameEdit.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .controls {
3 | clear: both;
4 | margin-top: 6px;
5 | }
6 |
7 | .field {
8 | background: #fff;
9 | border: 1px solid rgba(9, 30, 66, 0.13);
10 | border-radius: 3px;
11 | box-sizing: border-box;
12 | color: #17394d;
13 | display: block;
14 | font-size: 14px;
15 | line-height: 1.5;
16 | overflow: hidden;
17 | padding: 8px 12px;
18 | resize: none;
19 |
20 | &:focus {
21 | outline: none;
22 | }
23 | }
24 |
25 | .wrapper {
26 | padding: 9px 32px 16px 40px;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/reducers/socket.js:
--------------------------------------------------------------------------------
1 | import ActionTypes from '../constants/ActionTypes';
2 |
3 | const initialState = {
4 | isDisconnected: false,
5 | };
6 |
7 | // eslint-disable-next-line default-param-last
8 | export default (state = initialState, { type }) => {
9 | switch (type) {
10 | case ActionTypes.SOCKET_DISCONNECT_HANDLE:
11 | return {
12 | ...state,
13 | isDisconnected: true,
14 | };
15 | case ActionTypes.SOCKET_RECONNECT_HANDLE:
16 | return {
17 | ...state,
18 | isDisconnected: false,
19 | };
20 | default:
21 | return state;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/client/src/sagas/login/watchers/login.js:
--------------------------------------------------------------------------------
1 | import { all, takeEvery } from 'redux-saga/effects';
2 |
3 | import services from '../services';
4 | import EntryActionTypes from '../../../constants/EntryActionTypes';
5 |
6 | export default function* loginWatchers() {
7 | yield all([
8 | takeEvery(EntryActionTypes.AUTHENTICATE, ({ payload: { data } }) =>
9 | services.authenticate(data),
10 | ),
11 | takeEvery(EntryActionTypes.USING_OIDC_AUTHENTICATE, () => services.authenticateUsingOidc()),
12 | takeEvery(EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, () => services.clearAuthenticateError()),
13 | ]);
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/orm.js:
--------------------------------------------------------------------------------
1 | import { ORM } from 'redux-orm';
2 |
3 | import {
4 | Activity,
5 | Attachment,
6 | Board,
7 | BoardMembership,
8 | Card,
9 | Label,
10 | List,
11 | Notification,
12 | Project,
13 | ProjectManager,
14 | Task,
15 | User,
16 | } from './models';
17 |
18 | const orm = new ORM({
19 | stateSelector: (state) => state.orm,
20 | });
21 |
22 | orm.register(
23 | User,
24 | Project,
25 | ProjectManager,
26 | Board,
27 | BoardMembership,
28 | Label,
29 | List,
30 | Card,
31 | Task,
32 | Attachment,
33 | Activity,
34 | Notification,
35 | );
36 |
37 | export default orm;
38 |
--------------------------------------------------------------------------------
/client/src/locales/zh-CN/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: '邮箱或用户名',
5 | invalidEmailOrUsername: '无效的邮箱或用户名',
6 | invalidPassword: '密码错误',
7 | logInToPlanka: '登录至 Planka',
8 | noInternetConnection: '没有网络连接',
9 | pageNotFound_title: '找不到页面',
10 | password: '密码',
11 | projectManagement: '项目管理',
12 | serverConnectionFailed: '服务器连接失败',
13 | unknownError: '未知错误,请稍后重试',
14 | useSingleSignOn: '使用单点登录',
15 | },
16 |
17 | action: {
18 | logIn: '登录',
19 | logInWithSSO: '使用SSO登录',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/server/api/controllers/access-tokens/delete.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | async fn() {
3 | const { currentSession } = this.req;
4 |
5 | await Session.updateOne({
6 | id: currentSession.id,
7 | deletedAt: null,
8 | }).set({
9 | deletedAt: new Date().toISOString(),
10 | });
11 |
12 | sails.sockets.leaveAll(`@accessToken:${currentSession.accessToken}`);
13 |
14 | if (currentSession.httpOnlyToken && !this.req.isSocket) {
15 | sails.helpers.utils.clearHttpOnlyTokenCookie(this.res);
16 | }
17 |
18 | return {
19 | item: currentSession.accessToken,
20 | };
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/locales/zh-TW/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: '郵箱或使用者名稱',
5 | invalidEmailOrUsername: '無效的郵箱或使用者名稱',
6 | invalidPassword: '密碼錯誤',
7 | logInToPlanka: '登入至 Planka',
8 | noInternetConnection: '沒有網路連接',
9 | pageNotFound_title: '找不到頁面',
10 | password: '密碼',
11 | projectManagement: '專案管理',
12 | serverConnectionFailed: '伺服器連接失敗',
13 | unknownError: '未知錯誤,請稍後重試',
14 | useSingleSignOn: '使用單一登入',
15 | },
16 |
17 | action: {
18 | logIn: '登入',
19 | logInWithSSO: '使用SSO登入',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/server/api/helpers/utils/verify-jwt-token.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | module.exports = {
4 | sync: true,
5 |
6 | inputs: {
7 | token: {
8 | type: 'string',
9 | required: true,
10 | },
11 | },
12 |
13 | exits: {
14 | invalidToken: {},
15 | },
16 |
17 | fn(inputs) {
18 | let payload;
19 | try {
20 | payload = jwt.verify(inputs.token, sails.config.session.secret);
21 | } catch (error) {
22 | throw 'invalidToken';
23 | }
24 |
25 | return {
26 | subject: payload.sub,
27 | issuedAt: new Date(payload.iat * 1000),
28 | };
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722006570_create_task_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('task', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('card_id').notNullable();
8 |
9 | table.text('name').notNullable();
10 | table.boolean('is_completed').notNullable();
11 |
12 | table.timestamp('created_at', true);
13 | table.timestamp('updated_at', true);
14 |
15 | /* Indexes */
16 |
17 | table.index('card_id');
18 | });
19 |
20 | module.exports.down = (knex) => knex.schema.dropTable('task');
21 |
--------------------------------------------------------------------------------
/client/src/lib/popup/Popup.module.css:
--------------------------------------------------------------------------------
1 | .closeButton {
2 | background: transparent !important;
3 | box-shadow: none !important;
4 | margin: 0 !important;
5 | padding: 10px 12px 10px 8px !important;
6 | position: absolute;
7 | right: 0;
8 | top: 0;
9 | width: 40px;
10 | z-index: 2000;
11 | }
12 |
13 | .wrapper {
14 | border-radius: 3px !important;
15 | border-width: 0 !important;
16 | box-shadow: 0 8px 16px -4px rgba(9, 45, 66, 0.25),
17 | 0 0 0 1px rgba(9, 45, 66, 0.08) !important;
18 | margin-top: 6px !important;
19 | max-height: calc(100% - 70px);
20 | padding: 0 12px 12px !important;
21 | width: 304px;
22 | }
23 |
--------------------------------------------------------------------------------
/server/api/helpers/projects/get-manager-and-board-member-user-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | },
11 |
12 | async fn(inputs) {
13 | const projectManagerUserIds = await sails.helpers.projects.getManagerUserIds(inputs.idOrIds);
14 | const boardMemberUserIds = await sails.helpers.projects.getBoardMemberUserIds(inputs.idOrIds);
15 |
16 | return _.union(projectManagerUserIds, boardMemberUserIds);
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/server/db/migrations/20220815155645_add_permissions_to_board_membership_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = async (knex) => {
2 | await knex.schema.table('board_membership', (table) => {
3 | /* Columns */
4 |
5 | table.text('role').notNullable().defaultTo('editor');
6 | table.boolean('can_comment');
7 | });
8 |
9 | return knex.schema.alterTable('board_membership', (table) => {
10 | table.text('role').notNullable().alter();
11 | });
12 | };
13 |
14 | module.exports.down = (knex) =>
15 | knex.schema.table('board_membership', (table) => {
16 | table.dropColumn('role');
17 | table.dropColumn('can_comment');
18 | });
19 |
--------------------------------------------------------------------------------
/server/db/migrations/20221225224651_remove_board_types.js.js:
--------------------------------------------------------------------------------
1 | module.exports.up = async (knex) => {
2 | await knex.schema.table('board', (table) => {
3 | table.dropColumn('type');
4 | });
5 |
6 | return knex.schema.table('card', (table) => {
7 | table.dropNullable('list_id');
8 | });
9 | };
10 |
11 | module.exports.down = async (knex) => {
12 | await knex.schema.table('board', (table) => {
13 | /* Columns */
14 |
15 | table.text('type').notNullable().defaultTo('kanban'); // FIXME: drop default
16 | });
17 |
18 | return knex.schema.table('card', (table) => {
19 | table.setNullable('list_id');
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/client/src/components/Core/Core.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .message {
3 | background: #eb5a46;
4 | border-radius: 4px;
5 | bottom: 20px;
6 | box-shadow: #b04632 0 1px 0;
7 | max-width: calc(100% - 40px);
8 | padding: 12px 18px;
9 | position: fixed;
10 | right: 20px;
11 | width: 390px;
12 | z-index: 10001;
13 | }
14 |
15 | .messageContent {
16 | color: #fff;
17 | font-size: 16px;
18 | line-height: 1.4;
19 | }
20 |
21 | .messageHeader {
22 | color: #fff;
23 | font-size: 24px;
24 | font-weight: bold;
25 | line-height: 1.2;
26 | margin-bottom: 8px;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/hooks/use-steps.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | const createStep = (type, params = {}) => {
4 | if (!type) {
5 | return null;
6 | }
7 |
8 | return {
9 | type,
10 | params,
11 | };
12 | };
13 |
14 | export default (initialType, initialParams) => {
15 | const [step, setStep] = useState(() => createStep(initialType, initialParams));
16 |
17 | const open = useCallback((type, params) => {
18 | setStep(createStep(type, params));
19 | }, []);
20 |
21 | const handleBack = useCallback(() => {
22 | setStep(null);
23 | }, []);
24 |
25 | return [step, open, handleBack];
26 | };
27 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722005928_create_card_label_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('card_label', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('card_id').notNullable();
8 | table.bigInteger('label_id').notNullable();
9 |
10 | table.timestamp('created_at', true);
11 | table.timestamp('updated_at', true);
12 |
13 | /* Indexes */
14 |
15 | table.unique(['card_id', 'label_id']);
16 | table.index('label_id');
17 | });
18 |
19 | module.exports.down = (knex) => knex.schema.dropTable('card_label');
20 |
--------------------------------------------------------------------------------
/client/src/components/List/CardAdd.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .field {
3 | border: none;
4 | margin-bottom: 4px;
5 | outline: none;
6 | overflow: hidden;
7 | padding: 0;
8 | resize: none;
9 | width: 100%;
10 | }
11 |
12 | .fieldWrapper {
13 | background: #fff;
14 | border-radius: 3px;
15 | box-shadow: 0 1px 0 #ccc;
16 | margin-bottom: 8px;
17 | min-height: 20px;
18 | padding: 6px 8px 2px;
19 | }
20 |
21 | .submitButton {
22 | vertical-align: top;
23 | }
24 |
25 | .wrapper {
26 | padding-bottom: 8px;
27 | }
28 |
29 | .wrapperClosed {
30 | display: none;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722005359_create_card_membership_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('card_membership', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('card_id').notNullable();
8 | table.bigInteger('user_id').notNullable();
9 |
10 | table.timestamp('created_at', true);
11 | table.timestamp('updated_at', true);
12 |
13 | /* Indexes */
14 |
15 | table.unique(['card_id', 'user_id']);
16 | table.index('user_id');
17 | });
18 |
19 | module.exports.down = (knex) => knex.schema.dropTable('card_membership');
20 |
--------------------------------------------------------------------------------
/server/api/helpers/utils/map-records.js:
--------------------------------------------------------------------------------
1 | const recordsValidator = (value) => _.isArray(value);
2 |
3 | module.exports = {
4 | sync: true,
5 |
6 | inputs: {
7 | records: {
8 | type: 'ref',
9 | custom: recordsValidator,
10 | required: true,
11 | },
12 | attribute: {
13 | type: 'string',
14 | defaultsTo: 'id',
15 | },
16 | unique: {
17 | type: 'boolean',
18 | defaultsTo: false,
19 | },
20 | },
21 |
22 | fn(inputs) {
23 | let result = _.map(inputs.records, inputs.attribute);
24 | if (inputs.unique) {
25 | result = _.uniq(result);
26 | }
27 |
28 | return result;
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/server/db/migrations/20180721234154_create_project_manager_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('project_manager', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('project_id').notNullable();
8 | table.bigInteger('user_id').notNullable();
9 |
10 | table.timestamp('created_at', true);
11 | table.timestamp('updated_at', true);
12 |
13 | /* Indexes */
14 |
15 | table.unique(['project_id', 'user_id']);
16 | table.index('user_id');
17 | });
18 |
19 | module.exports.down = (knex) => knex.schema.dropTable('project_manager');
20 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722001747_create_board_membership_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('board_membership', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('board_id').notNullable();
8 | table.bigInteger('user_id').notNullable();
9 |
10 | table.timestamp('created_at', true);
11 | table.timestamp('updated_at', true);
12 |
13 | /* Indexes */
14 |
15 | table.unique(['board_id', 'user_id']);
16 | table.index('user_id');
17 | });
18 |
19 | module.exports.down = (knex) => knex.schema.dropTable('board_membership');
20 |
--------------------------------------------------------------------------------
/client/src/selectors/project-managers.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'redux-orm';
2 |
3 | import orm from '../orm';
4 |
5 | export const makeSelectProjectManagerById = () =>
6 | createSelector(
7 | orm,
8 | (_, id) => id,
9 | ({ ProjectManager }, id) => {
10 | const projectManagerModel = ProjectManager.withId(id);
11 |
12 | if (!projectManagerModel) {
13 | return projectManagerModel;
14 | }
15 |
16 | return projectManagerModel.ref;
17 | },
18 | );
19 |
20 | export const selectProjectManagerById = makeSelectProjectManagerById();
21 |
22 | export default {
23 | makeSelectProjectManagerById,
24 | selectProjectManagerById,
25 | };
26 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const board = await Board.findOne(inputs.criteria);
15 |
16 | if (!board) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const project = await Project.findOne(board.projectId);
21 |
22 | if (!project) {
23 | throw {
24 | pathNotFound: {
25 | board,
26 | },
27 | };
28 | }
29 |
30 | return {
31 | board,
32 | project,
33 | };
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/server/db/migrations/20180721021044_create_archive_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('archive', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.text('from_model').notNullable();
8 | table.bigInteger('original_record_id').notNullable();
9 | table.json('original_record').notNullable();
10 |
11 | table.timestamp('created_at', true);
12 | table.timestamp('updated_at', true);
13 |
14 | /* Indexes */
15 |
16 | table.unique(['from_model', 'original_record_id']);
17 | });
18 |
19 | module.exports.down = (knex) => knex.schema.dropTable('archive');
20 |
--------------------------------------------------------------------------------
/server/db/migrations/20181024220134_create_action_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('action', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('card_id').notNullable();
8 | table.bigInteger('user_id').notNullable();
9 |
10 | table.text('type').notNullable();
11 | table.jsonb('data').notNullable();
12 |
13 | table.timestamp('created_at', true);
14 | table.timestamp('updated_at', true);
15 |
16 | /* Indexes */
17 |
18 | table.index('card_id');
19 | });
20 |
21 | module.exports.down = (knex) => knex.schema.dropTable('action');
22 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722003502_create_list_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('list', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('board_id').notNullable();
8 |
9 | table.specificType('position', 'double precision').notNullable();
10 | table.text('name').notNullable();
11 |
12 | table.timestamp('created_at', true);
13 | table.timestamp('updated_at', true);
14 |
15 | /* Indexes */
16 |
17 | table.index('board_id');
18 | table.index('position');
19 | });
20 |
21 | module.exports.down = (knex) => knex.schema.dropTable('list');
22 |
--------------------------------------------------------------------------------
/client/src/components/Board/ListAdd.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .button {
3 | min-height: 30px;
4 | vertical-align: top;
5 | }
6 |
7 | .controls {
8 | margin-top: 4px;
9 | }
10 |
11 | .field {
12 | border: none;
13 | border-radius: 3px;
14 | box-shadow: 0 1px 0 #ccc;
15 | color: #333;
16 | outline: none;
17 | overflow: hidden;
18 | width: 100%;
19 |
20 | &:focus {
21 | border-color: #298fca;
22 | box-shadow: 0 0 2px #298fca;
23 | }
24 | }
25 |
26 | .wrapper {
27 | background: #e2e4e6;
28 | border-radius: 3px;
29 | padding: 4px;
30 | transition: opacity 40ms ease-in;
31 | width: 272px;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/locales/da-DK/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail eller brugernavn',
5 | invalidEmailOrUsername: 'Ugyldig e-mail eller brugernavn',
6 | invalidPassword: 'Ugyldig løsen',
7 | logInToPlanka: 'Log på Planka',
8 | noInternetConnection: 'Ingen forbindelse til internettet',
9 | pageNotFound_title: 'Side ej fundet',
10 | password: 'Løsen',
11 | projectManagement: 'Projektstyring',
12 | serverConnectionFailed: 'Ingen forbindelse til serveren',
13 | unknownError: 'Ukendt fejl - prøv igen',
14 | },
15 |
16 | action: {
17 | logIn: 'Log på',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/locales/ja-JP/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'Eメールまたはユーザー名',
5 | invalidEmailOrUsername: 'Eメールまたはユーザー名が無効',
6 | invalidPassword: 'パスワードが無効',
7 | logInToPlanka: 'Planka にログインする',
8 | noInternetConnection: 'インターネットに接続されていません',
9 | pageNotFound_title: 'ページが見つかりません',
10 | password: 'パスワード',
11 | projectManagement: 'プロジェクト管理',
12 | serverConnectionFailed: 'サーバーの接続に失敗',
13 | unknownError: '不明なエラーです。後でもう一度試してください。',
14 | useSingleSignOn: 'SSOを使用',
15 | },
16 |
17 | action: {
18 | logIn: 'ログイン',
19 | logInWithSSO: 'SSOでログイン',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Input/InputMask.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Input } from 'semantic-ui-react';
4 |
5 | import MaskedInput from './MaskedInput';
6 |
7 | const InputMask = React.forwardRef(({ mask, maskChar, ...props }, ref) => (
8 | // eslint-disable-next-line react/jsx-props-no-spreading
9 | } />
10 | ));
11 |
12 | InputMask.propTypes = {
13 | mask: PropTypes.string.isRequired,
14 | maskChar: PropTypes.string,
15 | };
16 |
17 | InputMask.defaultProps = {
18 | maskChar: undefined,
19 | };
20 |
21 | export default React.memo(InputMask);
22 |
--------------------------------------------------------------------------------
/client/src/models/index.js:
--------------------------------------------------------------------------------
1 | import User from './User';
2 | import Project from './Project';
3 | import ProjectManager from './ProjectManager';
4 | import Board from './Board';
5 | import BoardMembership from './BoardMembership';
6 | import Label from './Label';
7 | import List from './List';
8 | import Card from './Card';
9 | import Task from './Task';
10 | import Attachment from './Attachment';
11 | import Activity from './Activity';
12 | import Notification from './Notification';
13 |
14 | export {
15 | User,
16 | Project,
17 | ProjectManager,
18 | Board,
19 | BoardMembership,
20 | Label,
21 | List,
22 | Card,
23 | Task,
24 | Attachment,
25 | Activity,
26 | Notification,
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/selectors/board-memberships.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'redux-orm';
2 |
3 | import orm from '../orm';
4 |
5 | export const makeSelectBoardMembershipById = () =>
6 | createSelector(
7 | orm,
8 | (_, id) => id,
9 | ({ BoardMembership }, id) => {
10 | const boardMembershipModel = BoardMembership.withId(id);
11 |
12 | if (!boardMembershipModel) {
13 | return boardMembershipModel;
14 | }
15 |
16 | return boardMembershipModel.ref;
17 | },
18 | );
19 |
20 | export const selectBoardMembershipById = makeSelectBoardMembershipById();
21 |
22 | export default {
23 | makeSelectBoardMembershipById,
24 | selectBoardMembershipById,
25 | };
26 |
--------------------------------------------------------------------------------
/client/src/components/UserSettingsModal/AccountPane/AvatarEditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .action {
3 | border: none;
4 | border-radius: 0.28571429rem;
5 | display: inline-block;
6 | height: 36px;
7 | overflow: hidden;
8 | position: relative;
9 | transition: background 0.3s ease;
10 | width: 100%;
11 |
12 | &:hover {
13 | background: #e9e9e9;
14 | }
15 | }
16 |
17 | .actionButton {
18 | background: transparent;
19 | color: #6b808c;
20 | font-weight: normal;
21 | height: 36px;
22 | line-height: 24px;
23 | padding: 6px 12px;
24 | text-align: left;
25 | text-decoration: underline;
26 | width: 100%;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/tests/acceptance/pageObjects/DashboardPage.js:
--------------------------------------------------------------------------------
1 | class DashboardPage {
2 | constructor() {
3 | this.createProjectIconSelector = `.Projects_addTitle__tXhB4`;
4 | this.projectTitleInputSelector = `input[name="name"]`;
5 | this.createProjectButtonSelector = `//button[text()="Create project"]`;
6 | this.projectTitleSelector = `//div[@class="item Header_item__OOEY7 Header_title__l+wMf"][text()="%s"]`;
7 | }
8 |
9 | async createProject(project) {
10 | await page.click(this.createProjectIconSelector);
11 | await page.fill(this.projectTitleInputSelector, project);
12 | await page.click(this.createProjectButtonSelector);
13 | }
14 | }
15 |
16 | module.exports = DashboardPage;
17 |
--------------------------------------------------------------------------------
/client/tests/acceptance/stepDefinitions/dashBoardContext.js:
--------------------------------------------------------------------------------
1 | const { When, Then } = require('@cucumber/cucumber');
2 | const util = require('util');
3 | const { expect } = require('playwright/test');
4 |
5 | const DashboardPage = require('../pageObjects/DashboardPage');
6 |
7 | const dashboardPage = new DashboardPage();
8 |
9 | When('the user creates a project with name {string} using the webUI', async function (project) {
10 | await dashboardPage.createProject(project);
11 | });
12 |
13 | Then('the created project {string} should be opened', async function (project) {
14 | expect(
15 | await page.locator(util.format(dashboardPage.projectTitleSelector, project)),
16 | ).toBeVisible();
17 | });
18 |
--------------------------------------------------------------------------------
/server/api/controllers/notifications/update.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | ids: {
4 | type: 'string',
5 | required: true,
6 | regex: /^[0-9]+(,[0-9]+)*$/,
7 | },
8 | isRead: {
9 | type: 'boolean',
10 | },
11 | },
12 |
13 | async fn(inputs) {
14 | const { currentUser } = this.req;
15 |
16 | const values = _.pick(inputs, ['isRead']);
17 |
18 | const notifications = await sails.helpers.notifications.updateMany.with({
19 | values,
20 | recordsOrIds: inputs.ids.split(','),
21 | actorUser: currentUser,
22 | request: this.req,
23 | });
24 |
25 | return {
26 | items: notifications,
27 | };
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/client/src/locales/it-IT/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail o username',
5 | invalidEmailOrUsername: 'E-mail o username non valido',
6 | invalidPassword: 'Password non valida',
7 | logInToPlanka: 'Log in Planka',
8 | noInternetConnection: 'Nessuna connessione internet',
9 | pageNotFound_title: 'Pagina non trovata',
10 | password: 'Password',
11 | projectManagement: 'Gestione del progetto',
12 | serverConnectionFailed: 'Connesione al server fallita',
13 | unknownError: 'Errore sconosciuto, prova ancora',
14 | },
15 |
16 | action: {
17 | logIn: 'Log in',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/locales/sv-SE/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail eller användarnamn',
5 | invalidEmailOrUsername: 'Ogiltig e-mail eller användarnamn',
6 | invalidPassword: 'Ogiltigt lösenord',
7 | logInToPlanka: 'Logga in på Planka',
8 | noInternetConnection: 'Ingen internetanslutning',
9 | pageNotFound_title: 'Sidan Kunde Inte Hittas',
10 | password: 'Lösenord',
11 | projectManagement: 'Projektledning',
12 | serverConnectionFailed: 'Server connection failed',
13 | unknownError: 'Okänt fel, försök igen senare',
14 | },
15 |
16 | action: {
17 | logIn: 'Logga in',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/locales/tr-TR/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-posta adresi veya Kullanıcı adı',
5 | invalidEmailOrUsername: 'Geçersiz e-posta adresi veya kullanıcı adı',
6 | invalidPassword: 'Hatalı Şifre',
7 | logInToPlanka: 'Giriş Yap',
8 | noInternetConnection: 'Internet bağlantısı yok',
9 | pageNotFound_title: 'Sayfa bulunamadı',
10 | password: 'Şifre',
11 | projectManagement: 'Proje Yönetimi',
12 | serverConnectionFailed: 'Sunucu bağlantı hatası',
13 | unknownError: 'Bilinmeyen Hata, daha sonra tekrar deneyin',
14 | },
15 |
16 | action: {
17 | logIn: 'Giriş Yap',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const card = await Card.findOne(inputs.criteria);
15 |
16 | if (!card) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const path = await sails.helpers.lists
21 | .getProjectPath(card.listId)
22 | .intercept('pathNotFound', (nodes) => ({
23 | pathNotFound: {
24 | card,
25 | ...nodes,
26 | },
27 | }));
28 |
29 | return {
30 | card,
31 | ...path,
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-subscription-user-ids.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | exceptUserIdOrIds: {
11 | type: 'json',
12 | custom: idOrIdsValidator,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const cardSubscriptions = await sails.helpers.cards.getCardSubscriptions(
18 | inputs.idOrIds,
19 | inputs.exceptUserIdOrIds,
20 | );
21 |
22 | return sails.helpers.utils.mapRecords(cardSubscriptions, 'userId', _.isArray(inputs.idOrIds));
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/server/api/helpers/tasks/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const task = await Task.findOne(inputs.criteria);
15 |
16 | if (!task) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const path = await sails.helpers.cards
21 | .getProjectPath(task.cardId)
22 | .intercept('pathNotFound', (nodes) => ({
23 | pathNotFound: {
24 | task,
25 | ...nodes,
26 | },
27 | }));
28 |
29 | return {
30 | task,
31 | ...path,
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/server/api/helpers/utils/set-http-only-token-cookie.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | sync: true,
3 |
4 | inputs: {
5 | value: {
6 | type: 'string',
7 | required: true,
8 | },
9 | accessTokenPayload: {
10 | type: 'json',
11 | required: true,
12 | },
13 | response: {
14 | type: 'ref',
15 | required: true,
16 | },
17 | },
18 |
19 | fn(inputs) {
20 | inputs.response.cookie('httpOnlyToken', inputs.value, {
21 | expires: new Date(inputs.accessTokenPayload.exp * 1000),
22 | path: sails.config.custom.baseUrlPath,
23 | secure: sails.config.custom.baseUrlSecure,
24 | httpOnly: true,
25 | sameSite: 'strict',
26 | });
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/server/api/helpers/lists/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const list = await List.findOne(inputs.criteria);
15 |
16 | if (!list) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const path = await sails.helpers.boards
21 | .getProjectPath(list.boardId)
22 | .intercept('pathNotFound', (nodes) => ({
23 | pathNotFound: {
24 | list,
25 | ...nodes,
26 | },
27 | }));
28 |
29 | return {
30 | list,
31 | ...path,
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/client/src/components/CardModal/Activities/Item.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .author {
3 | color: #17394d;
4 | display: inline-block;
5 | font-weight: bold;
6 | line-height: 20px;
7 | }
8 |
9 | .content {
10 | border-bottom: 1px solid #092d4221;
11 | display: inline-block;
12 | padding-bottom: 14px;
13 | vertical-align: top;
14 | width: calc(100% - 40px);
15 | }
16 |
17 | .date {
18 | color: #6b808c;
19 | display: inline-block;
20 | font-size: 12px;
21 | line-height: 20px;
22 | }
23 |
24 | .text {
25 | line-height: 20px;
26 | }
27 |
28 | .user {
29 | display: inline-block;
30 | padding: 4px 8px 0 0;
31 | vertical-align: top;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/entry-actions/notifications.js:
--------------------------------------------------------------------------------
1 | import EntryActionTypes from '../constants/EntryActionTypes';
2 |
3 | const handleNotificationCreate = (notification) => ({
4 | type: EntryActionTypes.NOTIFICATION_CREATE_HANDLE,
5 | payload: {
6 | notification,
7 | },
8 | });
9 |
10 | const deleteNotification = (id) => ({
11 | type: EntryActionTypes.NOTIFICATION_DELETE,
12 | payload: {
13 | id,
14 | },
15 | });
16 |
17 | const handleNotificationDelete = (notification) => ({
18 | type: EntryActionTypes.NOTIFICATION_DELETE_HANDLE,
19 | payload: {
20 | notification,
21 | },
22 | });
23 |
24 | export default {
25 | handleNotificationCreate,
26 | deleteNotification,
27 | handleNotificationDelete,
28 | };
29 |
--------------------------------------------------------------------------------
/client/src/lib/redux-router/create-router-reducer.js:
--------------------------------------------------------------------------------
1 | import { LOCATION_CHANGE_HANDLE } from './actions';
2 |
3 | const createRouterReducer = (history) => {
4 | const initialState = {
5 | location: history.location,
6 | action: history.action,
7 | };
8 |
9 | return (state = initialState, { type, payload } = {}) => {
10 | if (type === LOCATION_CHANGE_HANDLE) {
11 | const { location, action, isFirstRendering } = payload;
12 |
13 | if (isFirstRendering) {
14 | return state;
15 | }
16 |
17 | return {
18 | ...state,
19 | location,
20 | action,
21 | };
22 | }
23 |
24 | return state;
25 | };
26 | };
27 |
28 | export default createRouterReducer;
29 |
--------------------------------------------------------------------------------
/client/src/locales/ko-KR/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: '이메일 혹은 사용자 이름',
5 | invalidEmailOrUsername: '이메일 혹은 사용자 이름이 유효하지 않습니다',
6 | invalidPassword: '유효하지 않은 비밀번호',
7 | logInToPlanka: 'Planka에 로그인',
8 | noInternetConnection: '인터넷 연결이 없음',
9 | pageNotFound_title: '페이지를 찾을 수 없습니다',
10 | password: '비밀번호',
11 | projectManagement: '프로젝트 관리',
12 | serverConnectionFailed: '서버 연결에 실패함',
13 | unknownError: '알 수 없는 오류, 나중에 다시 시도하십시오',
14 | useSingleSignOn: 'single sign-on(SSO) 사용',
15 | },
16 |
17 | action: {
18 | logIn: '로그인',
19 | logInWithSSO: 'single sign-on(SSO)으로 로그인',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/locales/pl-PL/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail lub nazwa użytkownika',
5 | invalidEmailOrUsername: 'Błędny e-mail lub nazwa użytkownika',
6 | invalidPassword: 'Błędne Hasło',
7 | logInToPlanka: 'Zaloguj do Planki',
8 | noInternetConnection: 'Brak połączenia z internetem',
9 | pageNotFound_title: 'Nie znaleziono strony',
10 | password: 'Hasło',
11 | projectManagement: 'Zarządzanie projektem',
12 | serverConnectionFailed: 'Błąd połączenia z serwerem',
13 | unknownError: 'Nieznany błąd, spróbuj ponownie później',
14 | },
15 |
16 | action: {
17 | logIn: 'Zaloguj',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/locales/uz-UZ/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail yoki foydalanuvchi nomi',
5 | invalidEmailOrUsername: "Noto'g'ri e-mail yoki foydalanuvchi nomi",
6 | invalidPassword: "Noto'g'ri parol",
7 | logInToPlanka: 'Planka ga Kirish',
8 | noInternetConnection: "Internet bog'lanishi yo'q",
9 | pageNotFound_title: 'Sahifa Topilmadi',
10 | password: 'Parol',
11 | projectManagement: 'Loyiha boshqaruvi',
12 | serverConnectionFailed: "Serverga bog'lanish xatosi",
13 | unknownError: "Noma'lum xatolik, qaytadan urinib ko'ring",
14 | },
15 |
16 | action: {
17 | logIn: 'Kirish',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-lists.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | exceptListIdOrIds: {
11 | type: 'json',
12 | custom: idOrIdsValidator,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const criteria = {
18 | boardId: inputs.idOrIds,
19 | };
20 |
21 | if (!_.isUndefined(inputs.exceptListIdOrIds)) {
22 | criteria.id = {
23 | '!=': inputs.exceptListIdOrIds,
24 | };
25 | }
26 |
27 | return sails.helpers.lists.getMany(criteria);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-tasks.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | exceptTaskIdOrIds: {
11 | type: 'json',
12 | custom: idOrIdsValidator,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const criteria = {
18 | cardId: inputs.idOrIds,
19 | };
20 |
21 | if (!_.isUndefined(inputs.exceptTaskIdOrIds)) {
22 | criteria.id = {
23 | '!=': inputs.exceptTaskIdOrIds,
24 | };
25 | }
26 |
27 | return sails.helpers.tasks.getMany(criteria);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/server/api/helpers/labels/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const label = await Label.findOne(inputs.criteria);
15 |
16 | if (!label) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const path = await sails.helpers.boards
21 | .getProjectPath(label.boardId)
22 | .intercept('pathNotFound', (nodes) => ({
23 | pathNotFound: {
24 | label,
25 | ...nodes,
26 | },
27 | }));
28 |
29 | return {
30 | label,
31 | ...path,
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/server/api/helpers/lists/get-cards.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | exceptCardIdOrIds: {
11 | type: 'json',
12 | custom: idOrIdsValidator,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const criteria = {
18 | listId: inputs.idOrIds,
19 | };
20 |
21 | if (!_.isUndefined(inputs.exceptCardIdOrIds)) {
22 | criteria.id = {
23 | '!=': inputs.exceptCardIdOrIds,
24 | };
25 | }
26 |
27 | return sails.helpers.cards.getMany(criteria);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722005122_create_card_subscription_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('card_subscription', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('card_id').notNullable();
8 | table.bigInteger('user_id').notNullable();
9 |
10 | table.boolean('is_permanent').notNullable();
11 |
12 | table.timestamp('created_at', true);
13 | table.timestamp('updated_at', true);
14 |
15 | /* Indexes */
16 |
17 | table.unique(['card_id', 'user_id']);
18 | table.index('user_id');
19 | });
20 |
21 | module.exports.down = (knex) => knex.schema.dropTable('card_subscription');
22 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Popup/PopupHeader.module.css:
--------------------------------------------------------------------------------
1 | .backButton {
2 | background: transparent !important;
3 | box-shadow: none !important;
4 | left: 0;
5 | margin: 0 !important;
6 | padding: 10px 8px 10px 12px !important;
7 | position: absolute;
8 | top: 0;
9 | width: 40px;
10 | z-index: 2000;
11 | }
12 |
13 | .content {
14 | border-bottom: 1px solid #eee;
15 | font-size: 14px;
16 | font-weight: normal;
17 | left: 0;
18 | line-height: 20px;
19 | margin: 0 12px;
20 | padding: 12px 28px 8px;
21 | position: absolute;
22 | right: 0;
23 | top: 0;
24 | }
25 |
26 | .wrapper {
27 | height: 40px;
28 | margin: 0 -12px 8px !important;
29 | position: relative;
30 | text-align: center;
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/locales/sk-SK/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail alebo používateľské meno',
5 | invalidEmailOrUsername: 'Nesprávny e-mail alebo používateľské meno',
6 | invalidPassword: 'Nesprávne heslo',
7 | logInToPlanka: 'Prihlásiť sa do Planka',
8 | noInternetConnection: 'Bez pripojenia k internetu',
9 | pageNotFound_title: 'Stránka neexistuje',
10 | password: 'Heslo',
11 | projectManagement: 'Správa projektu',
12 | serverConnectionFailed: 'Pripojenie k serveru zlyhalo',
13 | unknownError: 'Neznáma chyba, skúste to neskôr',
14 | },
15 |
16 | action: {
17 | logIn: 'Prihlásiť sa',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/server/api/helpers/actions/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const action = await Action.findOne(inputs.criteria);
15 |
16 | if (!action) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const path = await sails.helpers.cards
21 | .getProjectPath(action.cardId)
22 | .intercept('pathNotFound', (nodes) => ({
23 | pathNotFound: {
24 | action,
25 | ...nodes,
26 | },
27 | }));
28 |
29 | return {
30 | action,
31 | ...path,
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/server/api/helpers/boards/get-labels.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | exceptLabelIdOrIds: {
11 | type: 'json',
12 | custom: idOrIdsValidator,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const criteria = {
18 | boardId: inputs.idOrIds,
19 | };
20 |
21 | if (!_.isUndefined(inputs.exceptLabelIdOrIds)) {
22 | criteria.id = {
23 | '!=': inputs.exceptLabelIdOrIds,
24 | };
25 | }
26 |
27 | return sails.helpers.labels.getMany(criteria);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/client/src/components/UserSettingsModal/AboutPane.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTranslation } from 'react-i18next';
3 | import { Image, Tab } from 'semantic-ui-react';
4 |
5 | import version from '../../version';
6 |
7 | import logo from '../../assets/images/logo.png';
8 |
9 | import styles from './AboutPane.module.scss';
10 |
11 | const AboutPane = React.memo(() => {
12 | const [t] = useTranslation();
13 |
14 | return (
15 |
16 |
17 |
18 | {t('common.version')} {version}
19 |
20 |
21 | );
22 | });
23 |
24 | export default AboutPane;
25 |
--------------------------------------------------------------------------------
/server/api/helpers/projects/get-boards.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | exceptBoardIdOrIds: {
11 | type: 'json',
12 | custom: idOrIdsValidator,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const criteria = {
18 | projectId: inputs.idOrIds,
19 | };
20 |
21 | if (!_.isUndefined(inputs.exceptBoardIdOrIds)) {
22 | criteria.id = {
23 | '!=': inputs.exceptBoardIdOrIds,
24 | };
25 | }
26 |
27 | return sails.helpers.boards.getMany(criteria);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722000627_create_board_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('board', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('project_id').notNullable();
8 |
9 | table.text('type').notNullable();
10 | table.specificType('position', 'double precision').notNullable();
11 | table.text('name').notNullable();
12 |
13 | table.timestamp('created_at', true);
14 | table.timestamp('updated_at', true);
15 |
16 | /* Indexes */
17 |
18 | table.index('project_id');
19 | table.index('position');
20 | });
21 |
22 | module.exports.down = (knex) => knex.schema.dropTable('board');
23 |
--------------------------------------------------------------------------------
/client/src/components/Project/Project.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import BoardsContainer from '../../containers/BoardsContainer';
5 | import ProjectSettingsModalContainer from '../../containers/ProjectSettingsModalContainer';
6 |
7 | import styles from './Project.module.scss';
8 |
9 | const Project = React.memo(({ isSettingsModalOpened }) => {
10 | return (
11 | <>
12 |
13 |
14 |
15 | {isSettingsModalOpened && }
16 | >
17 | );
18 | });
19 |
20 | Project.propTypes = {
21 | isSettingsModalOpened: PropTypes.bool.isRequired,
22 | };
23 |
24 | export default Project;
25 |
--------------------------------------------------------------------------------
/client/src/containers/ProjectAddModalContainer.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 |
4 | import entryActions from '../entry-actions';
5 | import ProjectAddModal from '../components/ProjectAddModal';
6 |
7 | const mapStateToProps = ({
8 | ui: {
9 | projectCreateForm: { data: defaultData, isSubmitting },
10 | },
11 | }) => ({
12 | defaultData,
13 | isSubmitting,
14 | });
15 |
16 | const mapDispatchToProps = (dispatch) =>
17 | bindActionCreators(
18 | {
19 | onCreate: entryActions.createProject,
20 | onClose: entryActions.closeModal,
21 | },
22 | dispatch,
23 | );
24 |
25 | export default connect(mapStateToProps, mapDispatchToProps)(ProjectAddModal);
26 |
--------------------------------------------------------------------------------
/client/src/entry-actions/comment-activities.js:
--------------------------------------------------------------------------------
1 | import EntryActionTypes from '../constants/EntryActionTypes';
2 |
3 | const createCommentActivityInCurrentCard = (data) => ({
4 | type: EntryActionTypes.COMMENT_ACTIVITY_IN_CURRENT_CARD_CREATE,
5 | payload: {
6 | data,
7 | },
8 | });
9 |
10 | const updateCommentActivity = (id, data) => ({
11 | type: EntryActionTypes.COMMENT_ACTIVITY_UPDATE,
12 | payload: {
13 | id,
14 | data,
15 | },
16 | });
17 |
18 | const deleteCommentActivity = (id) => ({
19 | type: EntryActionTypes.COMMENT_ACTIVITY_DELETE,
20 | payload: {
21 | id,
22 | },
23 | });
24 |
25 | export default {
26 | createCommentActivityInCurrentCard,
27 | updateCommentActivity,
28 | deleteCommentActivity,
29 | };
30 |
--------------------------------------------------------------------------------
/client/src/locales/de-DE/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-Mail-Adresse oder Benutzername',
5 | invalidEmailOrUsername: 'Ungültige E-Mail-Adresse oder Benutzername',
6 | invalidPassword: 'Ungültiges Passwort',
7 | logInToPlanka: 'Einloggen',
8 | noInternetConnection: 'Keine Internetverbindung',
9 | pageNotFound_title: 'Seite nicht gefunden',
10 | password: 'Passwort',
11 | projectManagement: 'Projekt-Management',
12 | serverConnectionFailed: 'Serververbindung fehlgeschlagen',
13 | unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
14 | },
15 |
16 | action: {
17 | logIn: 'Einloggen',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/locales/es-ES/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'Correo o nombre de usuario',
5 | invalidEmailOrUsername: 'Correo o nombre de usuario incorrecto',
6 | invalidPassword: 'Contraseña incorrecta',
7 | logInToPlanka: 'Iniciar sesión en Planka',
8 | noInternetConnection: 'Sin conexión a internet',
9 | pageNotFound_title: 'Página no encontrada',
10 | password: 'Contraseña',
11 | projectManagement: 'Gestión de Proyectos',
12 | serverConnectionFailed: 'Conexión con el servidor fallida',
13 | unknownError: 'Error desconocido, intenta más tarde',
14 | },
15 |
16 | action: {
17 | logIn: 'Iniciar sesión',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/utils/merge-records.js:
--------------------------------------------------------------------------------
1 | const mergeRecords = (target, ...sources) => {
2 | if (sources.length === 0) {
3 | return target;
4 | }
5 |
6 | const source = sources.shift();
7 |
8 | if (!target || !source) {
9 | return mergeRecords(target || source, ...sources);
10 | }
11 |
12 | const nextTarget = [...target];
13 |
14 | source.forEach((sourceRecord) => {
15 | const index = nextTarget.findIndex((targetRecord) => targetRecord.id === sourceRecord.id);
16 |
17 | if (index >= 0) {
18 | Object.assign(nextTarget[index], sourceRecord);
19 | } else {
20 | nextTarget.push(sourceRecord);
21 | }
22 | });
23 |
24 | return mergeRecords(nextTarget, ...sources);
25 | };
26 |
27 | export default mergeRecords;
28 |
--------------------------------------------------------------------------------
/client/src/containers/UserAddStepContainer.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 |
4 | import entryActions from '../entry-actions';
5 | import UserAddStep from '../components/UserAddStep';
6 |
7 | const mapStateToProps = ({
8 | ui: {
9 | userCreateForm: { data: defaultData, isSubmitting, error },
10 | },
11 | }) => ({
12 | defaultData,
13 | isSubmitting,
14 | error,
15 | });
16 |
17 | const mapDispatchToProps = (dispatch) =>
18 | bindActionCreators(
19 | {
20 | onCreate: entryActions.createUser,
21 | onMessageDismiss: entryActions.clearUserCreateError,
22 | },
23 | dispatch,
24 | );
25 |
26 | export default connect(mapStateToProps, mapDispatchToProps)(UserAddStep);
27 |
--------------------------------------------------------------------------------
/server/api/helpers/cards/get-card-subscriptions.js:
--------------------------------------------------------------------------------
1 | const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
2 |
3 | module.exports = {
4 | inputs: {
5 | idOrIds: {
6 | type: 'json',
7 | custom: idOrIdsValidator,
8 | required: true,
9 | },
10 | exceptUserIdOrIds: {
11 | type: 'json',
12 | custom: idOrIdsValidator,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const criteria = {
18 | cardId: inputs.idOrIds,
19 | };
20 |
21 | if (!_.isUndefined(inputs.exceptUserIdOrIds)) {
22 | criteria.userId = {
23 | '!=': inputs.exceptUserIdOrIds,
24 | };
25 | }
26 |
27 | return sails.helpers.cardSubscriptions.getMany(criteria);
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/server/api/responses/conflict.js:
--------------------------------------------------------------------------------
1 | /**
2 | * conflict.js
3 | *
4 | * A custom response.
5 | *
6 | * Example usage:
7 | * ```
8 | * return res.conflict();
9 | * // -or-
10 | * return res.conflict(optionalData);
11 | * ```
12 | *
13 | * Or with actions2:
14 | * ```
15 | * exits: {
16 | * somethingHappened: {
17 | * responseType: 'conflict'
18 | * }
19 | * }
20 | * ```
21 | *
22 | * ```
23 | * throw 'somethingHappened';
24 | * // -or-
25 | * throw { somethingHappened: optionalData }
26 | * ```
27 | */
28 |
29 | module.exports = function conflict(message) {
30 | const { res } = this;
31 |
32 | return res.status(409).json({
33 | code: 'E_CONFLICT',
34 | message,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/server/api/helpers/attachments/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const attachment = await Attachment.findOne(inputs.criteria);
15 |
16 | if (!attachment) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const path = await sails.helpers.cards
21 | .getProjectPath(attachment.cardId)
22 | .intercept('pathNotFound', (nodes) => ({
23 | pathNotFound: {
24 | attachment,
25 | ...nodes,
26 | },
27 | }));
28 |
29 | return {
30 | attachment,
31 | ...path,
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/server/api/responses/notFound.js:
--------------------------------------------------------------------------------
1 | /**
2 | * notFound.js
3 | *
4 | * A custom response.
5 | *
6 | * Example usage:
7 | * ```
8 | * return res.notFound();
9 | * // -or-
10 | * return res.notFound(optionalData);
11 | * ```
12 | *
13 | * Or with actions2:
14 | * ```
15 | * exits: {
16 | * somethingHappened: {
17 | * responseType: 'notFound'
18 | * }
19 | * }
20 | * ```
21 | *
22 | * ```
23 | * throw 'somethingHappened';
24 | * // -or-
25 | * throw { somethingHappened: optionalData }
26 | * ```
27 | */
28 |
29 | module.exports = function notFound(message) {
30 | const { res } = this;
31 |
32 | return res.status(404).json({
33 | code: 'E_NOT_FOUND',
34 | message,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/server/config/policies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Policy Mappings
3 | * (sails.config.policies)
4 | *
5 | * Policies are simple functions which run **before** your actions.
6 | *
7 | * For more information on configuring policies, check out:
8 | * https://sailsjs.com/docs/concepts/policies
9 | */
10 |
11 | module.exports.policies = {
12 | /**
13 | *
14 | * Default policy for all controllers and actions, unless overridden.
15 | * (`true` allows public access)
16 | *
17 | */
18 |
19 | '*': 'is-authenticated',
20 |
21 | 'users/create': ['is-authenticated', 'is-admin'],
22 | 'users/delete': ['is-authenticated', 'is-admin'],
23 |
24 | 'show-config': true,
25 | 'access-tokens/create': true,
26 | 'access-tokens/exchange-using-oidc': true,
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/containers/CoreContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 |
3 | import selectors from '../selectors';
4 | import Core from '../components/Core';
5 |
6 | const mapStateToProps = (state) => {
7 | const isInitializing = selectors.selectIsInitializing(state);
8 | const isSocketDisconnected = selectors.selectIsSocketDisconnected(state);
9 | const currentModal = selectors.selectCurrentModal(state);
10 | const currentProject = selectors.selectCurrentProject(state);
11 | const currentBoard = selectors.selectCurrentBoard(state);
12 |
13 | return {
14 | isInitializing,
15 | isSocketDisconnected,
16 | currentModal,
17 | currentProject,
18 | currentBoard,
19 | };
20 | };
21 |
22 | export default connect(mapStateToProps)(Core);
23 |
--------------------------------------------------------------------------------
/server/api/responses/forbidden.js:
--------------------------------------------------------------------------------
1 | /**
2 | * forbidden.js
3 | *
4 | * A custom response.
5 | *
6 | * Example usage:
7 | * ```
8 | * return res.forbidden();
9 | * // -or-
10 | * return res.forbidden(optionalData);
11 | * ```
12 | *
13 | * Or with actions2:
14 | * ```
15 | * exits: {
16 | * somethingHappened: {
17 | * responseType: 'forbidden'
18 | * }
19 | * }
20 | * ```
21 | *
22 | * ```
23 | * throw 'somethingHappened';
24 | * // -or-
25 | * throw { somethingHappened: optionalData }
26 | * ```
27 | */
28 |
29 | module.exports = function forbidden(message) {
30 | const { res } = this;
31 |
32 | return res.status(403).json({
33 | code: 'E_FORBIDDEN',
34 | message,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/client/src/sagas/core/watchers/notifications.js:
--------------------------------------------------------------------------------
1 | import { all, takeEvery } from 'redux-saga/effects';
2 |
3 | import services from '../services';
4 | import EntryActionTypes from '../../../constants/EntryActionTypes';
5 |
6 | export default function* notificationsWatchers() {
7 | yield all([
8 | takeEvery(EntryActionTypes.NOTIFICATION_CREATE_HANDLE, ({ payload: { notification } }) =>
9 | services.handleNotificationCreate(notification),
10 | ),
11 | takeEvery(EntryActionTypes.NOTIFICATION_DELETE, ({ payload: { id } }) =>
12 | services.deleteNotification(id),
13 | ),
14 | takeEvery(EntryActionTypes.NOTIFICATION_DELETE_HANDLE, ({ payload: { notification } }) =>
15 | services.handleNotificationDelete(notification),
16 | ),
17 | ]);
18 | }
19 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-many.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | },
9 | withDeleted: {
10 | type: 'boolean',
11 | defaultsTo: false,
12 | },
13 | },
14 |
15 | async fn(inputs) {
16 | const criteria = {};
17 |
18 | if (_.isArray(inputs.criteria)) {
19 | criteria.id = inputs.criteria;
20 | } else if (_.isPlainObject(inputs.criteria)) {
21 | Object.assign(criteria, inputs.criteria);
22 | }
23 |
24 | if (!inputs.withDeleted) {
25 | criteria.deletedAt = null;
26 | }
27 |
28 | return User.find(criteria).sort('id');
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/Dockerfile.base:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine
2 |
3 | ARG VIPS_VERSION=8.14.5
4 |
5 | RUN apk -U upgrade \
6 | && apk add \
7 | bash pkgconf \
8 | libjpeg-turbo libexif librsvg cgif tiff libspng libimagequant \
9 | --no-cache \
10 | && apk add \
11 | build-base gobject-introspection-dev meson \
12 | libjpeg-turbo-dev libexif-dev librsvg-dev cgif-dev tiff-dev libspng-dev libimagequant-dev \
13 | --virtual vips-dependencies \
14 | --no-cache \
15 | && wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz | tar xJC /tmp \
16 | && cd /tmp/vips-${VIPS_VERSION} \
17 | && meson setup build-dir \
18 | && cd build-dir \
19 | && ninja \
20 | && ninja test \
21 | && ninja install \
22 | && rm -rf /tmp/vips-${VIPS_VERSION}
23 |
--------------------------------------------------------------------------------
/client/src/components/Memberships/Memberships.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .addUser {
3 | background: rgba(0, 0, 0, 0.24);
4 | border-radius: 50%;
5 | box-shadow: none;
6 | color: #fff;
7 | line-height: 36px;
8 | margin: 0;
9 | padding: 0;
10 | transition: all 0.1s ease 0s;
11 | vertical-align: top;
12 | width: 36px;
13 |
14 | @media only screen and (max-width: 797px) {
15 | margin-left: 10px;
16 | }
17 |
18 | &:hover {
19 | background: rgba(0, 0, 0, 0.32);
20 | }
21 | }
22 |
23 | .user {
24 | display: inline-block;
25 | margin: 0 -4px 0 0;
26 | vertical-align: top;
27 | line-height: 0;
28 | }
29 |
30 | .users {
31 | display: inline-block;
32 | vertical-align: top;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/components/UserSettingsModal/AccountPane/AccountPane.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .action {
3 | border: none;
4 | border-radius: 0.28571429rem;
5 | display: inline-block;
6 | height: 36px;
7 | overflow: hidden;
8 | position: relative;
9 | transition: background 0.3s ease;
10 | width: 100%;
11 |
12 | &:hover {
13 | background: #e9e9e9;
14 | }
15 | }
16 |
17 | .actionButton {
18 | background: transparent;
19 | color: #6b808c;
20 | font-weight: normal;
21 | height: 36px;
22 | line-height: 24px;
23 | padding: 6px 12px;
24 | text-align: left;
25 | text-decoration: underline;
26 | width: 100%;
27 | }
28 |
29 | .wrapper {
30 | border: none;
31 | box-shadow: none;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/components/ProjectSettingsModal/GeneralPane/GeneralPane.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .action {
3 | border: none;
4 | border-radius: 0.28571429rem;
5 | display: inline-block;
6 | height: 36px;
7 | overflow: hidden;
8 | position: relative;
9 | transition: background 0.3s ease;
10 | width: 100%;
11 |
12 | &:hover {
13 | background: #e9e9e9;
14 | }
15 | }
16 |
17 | .actionButton {
18 | background: transparent;
19 | color: #6b808c;
20 | font-weight: normal;
21 | height: 36px;
22 | line-height: 24px;
23 | padding: 6px 12px;
24 | text-align: left;
25 | text-decoration: underline;
26 | width: 100%;
27 | }
28 |
29 | .wrapper {
30 | border: none;
31 | box-shadow: none;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/locales/fa-IR/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'ایمیل یا نام کاربری',
5 | invalidEmailOrUsername: 'ایمیل یا نام کاربری نامعتبر است',
6 | invalidPassword: 'رمز عبور نامعتبر است',
7 | logInToPlanka: 'ورود به Planka',
8 | noInternetConnection: 'بدون اتصال به اینترنت',
9 | pageNotFound_title: 'صفحه یافت نشد',
10 | password: 'رمز عبور',
11 | projectManagement: 'مدیریت پروژه',
12 | serverConnectionFailed: 'اتصال به سرور ناموفق بود',
13 | unknownError: 'خطای ناشناخته، بعداً دوباره تلاش کنید',
14 | useSingleSignOn: 'استفاده از ورود یکپارچه',
15 | },
16 |
17 | action: {
18 | logIn: 'ورود',
19 | logInWithSSO: 'ورود با SSO',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/sagas/core/watchers/comment-activities.js:
--------------------------------------------------------------------------------
1 | import { all, takeEvery } from 'redux-saga/effects';
2 |
3 | import services from '../services';
4 | import EntryActionTypes from '../../../constants/EntryActionTypes';
5 |
6 | export default function* commentActivitiesWatchers() {
7 | yield all([
8 | takeEvery(EntryActionTypes.COMMENT_ACTIVITY_IN_CURRENT_CARD_CREATE, ({ payload: { data } }) =>
9 | services.createCommentActivityInCurrentCard(data),
10 | ),
11 | takeEvery(EntryActionTypes.COMMENT_ACTIVITY_UPDATE, ({ payload: { id, data } }) =>
12 | services.updateCommentActivity(id, data),
13 | ),
14 | takeEvery(EntryActionTypes.COMMENT_ACTIVITY_DELETE, ({ payload: { id } }) =>
15 | services.deleteCommentActivity(id),
16 | ),
17 | ]);
18 | }
19 |
--------------------------------------------------------------------------------
/server/api/responses/unauthorized.js:
--------------------------------------------------------------------------------
1 | /**
2 | * unauthorized.js
3 | *
4 | * A custom response.
5 | *
6 | * Example usage:
7 | * ```
8 | * return res.unauthorized();
9 | * // -or-
10 | * return res.unauthorized(optionalData);
11 | * ```
12 | *
13 | * Or with actions2:
14 | * ```
15 | * exits: {
16 | * somethingHappened: {
17 | * responseType: 'unauthorized'
18 | * }
19 | * }
20 | * ```
21 | *
22 | * ```
23 | * throw 'somethingHappened';
24 | * // -or-
25 | * throw { somethingHappened: optionalData }
26 | * ```
27 | */
28 |
29 | module.exports = function unauthorized(message) {
30 | const { res } = this;
31 |
32 | return res.status(401).json({
33 | code: 'E_UNAUTHORIZED',
34 | message,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: meltyshev
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/client/src/components/StopwatchEditStep/StopwatchEditStep.module.scss:
--------------------------------------------------------------------------------
1 | :global(#app) {
2 | .deleteButton {
3 | bottom: 12px;
4 | box-shadow: 0 1px 0 #cbcccc;
5 | position: absolute;
6 | right: 9px;
7 | }
8 |
9 | .fieldBox {
10 | display: inline-block;
11 | margin: 0 4px 12px;
12 | width: calc(33.3333% - 22px);
13 | }
14 |
15 | .fieldWrapper {
16 | margin: 0 -4px;
17 | }
18 |
19 | .iconButton {
20 | background: transparent;
21 | box-shadow: none;
22 | margin: 0 4px 0 1px;
23 | width: 36px;
24 |
25 | &:hover {
26 | background: #e9e9e9;
27 | }
28 | }
29 |
30 | .text {
31 | color: #444444;
32 | font-size: 12px;
33 | font-weight: bold;
34 | padding-bottom: 4px;
35 | padding-left: 2px;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/lib/custom-ui/components/Popup/PopupHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
4 |
5 | import styles from './PopupHeader.module.css';
6 |
7 | const PopupHeader = React.memo(({ children, onBack }) => (
8 |
9 | {onBack && }
10 | {children}
11 |
12 | ));
13 |
14 | PopupHeader.propTypes = {
15 | children: PropTypes.node.isRequired,
16 | onBack: PropTypes.func,
17 | };
18 |
19 | PopupHeader.defaultProps = {
20 | onBack: undefined,
21 | };
22 |
23 | export default PopupHeader;
24 |
--------------------------------------------------------------------------------
/server/api/helpers/board-memberships/get-project-path.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | inputs: {
3 | criteria: {
4 | type: 'json',
5 | required: true,
6 | },
7 | },
8 |
9 | exits: {
10 | pathNotFound: {},
11 | },
12 |
13 | async fn(inputs) {
14 | const boardMembership = await BoardMembership.findOne(inputs.criteria);
15 |
16 | if (!boardMembership) {
17 | throw 'pathNotFound';
18 | }
19 |
20 | const path = await sails.helpers.boards
21 | .getProjectPath(boardMembership.boardId)
22 | .intercept('pathNotFound', (nodes) => ({
23 | pathNotFound: {
24 | boardMembership,
25 | ...nodes,
26 | },
27 | }));
28 |
29 | return {
30 | boardMembership,
31 | ...path,
32 | };
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/server/api/helpers/users/get-one.js:
--------------------------------------------------------------------------------
1 | const criteriaValidator = (value) => _.isString(value) || _.isPlainObject(value);
2 |
3 | module.exports = {
4 | inputs: {
5 | criteria: {
6 | type: 'json',
7 | custom: criteriaValidator,
8 | required: true,
9 | },
10 | withDeleted: {
11 | type: 'boolean',
12 | defaultsTo: false,
13 | },
14 | },
15 |
16 | async fn(inputs) {
17 | const criteria = {};
18 |
19 | if (_.isString(inputs.criteria)) {
20 | criteria.id = inputs.criteria;
21 | } else if (_.isPlainObject(inputs.criteria)) {
22 | Object.assign(criteria, inputs.criteria);
23 | }
24 |
25 | if (!inputs.withDeleted) {
26 | criteria.deletedAt = null;
27 | }
28 |
29 | return User.findOne(criteria);
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/server/db/migrations/20180722006688_create_attachment_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('attachment', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('card_id').notNullable();
8 | table.bigInteger('creator_user_id').notNullable();
9 |
10 | table.text('dirname').notNullable();
11 | table.text('filename').notNullable();
12 | table.boolean('is_image').notNullable();
13 | table.text('name').notNullable();
14 |
15 | table.timestamp('created_at', true);
16 | table.timestamp('updated_at', true);
17 |
18 | /* Indexes */
19 |
20 | table.index('card_id');
21 | });
22 |
23 | module.exports.down = (knex) => knex.schema.dropTable('attachment');
24 |
--------------------------------------------------------------------------------
/client/src/locales/id-ID/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail atau username',
5 | invalidEmailOrUsername: 'E-mail atau username salah',
6 | invalidPassword: 'Kata sandi salah',
7 | logInToPlanka: 'Masuk ke Planka',
8 | noInternetConnection: 'Tidak ada koneksi internet',
9 | pageNotFound_title: 'Halaman Tidak Ditemukan',
10 | password: 'Kata sandi',
11 | projectManagement: 'Manajemen projek',
12 | serverConnectionFailed: 'Koneksi server gagal',
13 | unknownError: 'Kesalahan tidak diketahui, coba lagi nanti.',
14 | useSingleSignOn: 'Gunakan single sign-on',
15 | },
16 |
17 | action: {
18 | logIn: 'Masuk',
19 | logInWithSSO: 'Masuk dengan SSO',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/server/db/migrations/20220906094517_create_session_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('session', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('user_id').notNullable();
8 |
9 | table.text('access_token').notNullable();
10 | table.text('remote_address').notNullable();
11 | table.text('user_agent');
12 |
13 | table.timestamp('created_at', true);
14 | table.timestamp('updated_at', true);
15 | table.timestamp('deleted_at', true);
16 |
17 | /* Indexes */
18 |
19 | table.index('user_id');
20 | table.unique('access_token');
21 | table.index('remote_address');
22 | });
23 |
24 | module.exports.down = (knex) => knex.schema.dropTable('session');
25 |
--------------------------------------------------------------------------------
/server/api/helpers/utils/send-email.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // TODO: make sync?
3 |
4 | inputs: {
5 | to: {
6 | type: 'string',
7 | required: true,
8 | },
9 | subject: {
10 | type: 'string',
11 | required: true,
12 | },
13 | html: {
14 | type: 'string',
15 | required: true,
16 | },
17 | },
18 |
19 | async fn(inputs) {
20 | const transporter = sails.hooks.smtp.getTransporter(); // TODO: check if active?
21 |
22 | try {
23 | const info = await transporter.sendMail({
24 | ...inputs,
25 | from: sails.config.custom.smtpFrom,
26 | });
27 |
28 | sails.log.info(`Email sent: ${info.messageId}`);
29 | } catch (error) {
30 | sails.log.error(`Error sending email: ${error}`);
31 | }
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/client/src/hooks/use-closable-form.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from 'react';
2 |
3 | export default (close, isOpened = true) => {
4 | const isClosable = useRef(null);
5 |
6 | const handleFieldBlur = useCallback(() => {
7 | if (isClosable.current) {
8 | close();
9 | }
10 | }, [close]);
11 |
12 | const handleControlMouseOver = useCallback(() => {
13 | isClosable.current = false;
14 | }, []);
15 |
16 | const handleControlMouseOut = useCallback(() => {
17 | isClosable.current = true;
18 | }, []);
19 |
20 | useEffect(() => {
21 | if (isOpened) {
22 | isClosable.current = true;
23 | } else {
24 | isClosable.current = null;
25 | }
26 | }, [isOpened]);
27 |
28 | return [handleFieldBlur, handleControlMouseOver, handleControlMouseOut];
29 | };
30 |
--------------------------------------------------------------------------------
/server/db/migrations/20181112104653_create_notification_table.js:
--------------------------------------------------------------------------------
1 | module.exports.up = (knex) =>
2 | knex.schema.createTable('notification', (table) => {
3 | /* Columns */
4 |
5 | table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
6 |
7 | table.bigInteger('user_id').notNullable();
8 | table.bigInteger('action_id').notNullable();
9 | table.bigInteger('card_id').notNullable();
10 |
11 | table.boolean('is_read').notNullable();
12 |
13 | table.timestamp('created_at', true);
14 | table.timestamp('updated_at', true);
15 |
16 | /* Indexes */
17 |
18 | table.index('user_id');
19 | table.index('action_id');
20 | table.index('card_id');
21 | table.index('is_read');
22 | });
23 |
24 | module.exports.down = (knex) => knex.schema.dropTable('notification');
25 |
--------------------------------------------------------------------------------
/client/src/locales/ru-RU/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail или имя пользователя',
5 | invalidEmailOrUsername: 'Неверный e-mail или имя пользователя',
6 | invalidPassword: 'Неверный пароль',
7 | logInToPlanka: 'Вход в Planka',
8 | noInternetConnection: 'Нет соединения',
9 | pageNotFound_title: 'Страница не найдена',
10 | password: 'Пароль',
11 | projectManagement: 'Управление проектами',
12 | serverConnectionFailed: 'Не могу подключиться к серверу',
13 | unknownError: 'Что-то пошло не так, попробуйте позже',
14 | useSingleSignOn: 'Используйте единый вход',
15 | },
16 |
17 | action: {
18 | logIn: 'Войти',
19 | logInWithSSO: 'Войти с помощью единого входа',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/charts/planka/templates/secret-oidc.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.oidc.enabled }}
2 | {{- if eq (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (not (empty .Values.oidc.existingSecret)) -}}
3 | {{- fail "Either specify inline `clientId` and `clientSecret` or refer to them via `existingSecret`" -}}
4 | {{- end }}
5 | {{- if (and (and (not (empty .Values.oidc.clientId)) (not (empty .Values.oidc.clientSecret))) (empty .Values.oidc.existingSecret)) -}}
6 | apiVersion: v1
7 | kind: Secret
8 | metadata:
9 | name: {{ include "planka.fullname" . }}-oidc
10 | labels:
11 | {{- include "planka.labels" . | nindent 4 }}
12 | type: Opaque
13 | data:
14 | clientId: {{ .Values.oidc.clientId | b64enc | quote }}
15 | clientSecret: {{ .Values.oidc.clientSecret | b64enc | quote }}
16 | {{- end }}
17 | {{- end }}
18 |
--------------------------------------------------------------------------------
/client/src/locales/pt-BR/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail ou nome de usuário',
5 | invalidEmailOrUsername: 'E-mail ou nome de usuário inválido',
6 | invalidPassword: 'Senha inválida',
7 | logInToPlanka: 'Entrar no Planka',
8 | noInternetConnection: 'Sem conexão com a internet',
9 | pageNotFound_title: 'Página não encontrada',
10 | password: 'Senha',
11 | projectManagement: 'Gerenciamento de projetos',
12 | serverConnectionFailed: 'Falha na conexão com o servidor',
13 | unknownError: 'Erro desconhecido, tente novamente mais tarde',
14 | useSingleSignOn: 'Usar login único',
15 | },
16 |
17 | action: {
18 | logIn: 'Entrar',
19 | logInWithSSO: 'Entrar com SSO',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/locales/bg-BG/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'Имейл или потребителско име',
5 | invalidEmailOrUsername: 'Невалиден имейл или потребителско име',
6 | invalidPassword: 'Невалидна парола',
7 | logInToPlanka: 'Влезте в Planka',
8 | noInternetConnection: 'Няма интернет връзка',
9 | pageNotFound_title: 'Страницата не е намерена',
10 | password: 'Парола',
11 | projectManagement: 'Управление на проекти',
12 | serverConnectionFailed: 'Неуспешна връзка със сървъра',
13 | unknownError: 'Неизвестна грешка, опитайте отново по-късно',
14 | useSingleSignOn: 'Използване на single sign-on',
15 | },
16 |
17 | action: {
18 | logIn: 'Вход',
19 | logInWithSSO: 'Вход чрез SSO',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | setup:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 |
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: 18
19 | cache: 'npm'
20 |
21 | - name: Cache Node.js modules
22 | uses: actions/cache@v3
23 | with:
24 | path: client/node_modules
25 | key: ${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}
26 | restore-keys: |
27 | ${{ runner.os }}-node-
28 |
29 | - name: Install dependencies
30 | run: npm install
31 |
32 | - name: Run linter
33 | run: npm run lint
34 |
--------------------------------------------------------------------------------
/client/src/locales/en-GB/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail or username',
5 | invalidEmailOrUsername: 'Invalid e-mail or username',
6 | invalidCredentials: 'Invalid credentials',
7 | invalidPassword: 'Invalid password',
8 | logInToPlanka: 'Log in to Planka',
9 | noInternetConnection: 'No internet connection',
10 | pageNotFound_title: 'Page Not Found',
11 | password: 'Password',
12 | projectManagement: 'Project management',
13 | serverConnectionFailed: 'Server connection failed',
14 | unknownError: 'Unknown error, try again later',
15 | useSingleSignOn: 'Use single sign-on',
16 | },
17 |
18 | action: {
19 | logIn: 'Log in',
20 | logInWithSSO: 'Log in with SSO',
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/client/src/locales/en-US/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail or username',
5 | invalidEmailOrUsername: 'Invalid e-mail or username',
6 | invalidCredentials: 'Invalid credentials',
7 | invalidPassword: 'Invalid password',
8 | logInToPlanka: 'Log in to Planka',
9 | noInternetConnection: 'No internet connection',
10 | pageNotFound_title: 'Page Not Found',
11 | password: 'Password',
12 | projectManagement: 'Project management',
13 | serverConnectionFailed: 'Server connection failed',
14 | unknownError: 'Unknown error, try again later',
15 | useSingleSignOn: 'Use single sign-on',
16 | },
17 |
18 | action: {
19 | logIn: 'Log in',
20 | logInWithSSO: 'Log in with SSO',
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/client/src/locales/hu-HU/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail vagy felhasználó',
5 | invalidEmailOrUsername: 'Érvénytelen e-mail vagy felhasználó',
6 | invalidPassword: 'Érvénytelen jelszó',
7 | logInToPlanka: 'Plankába belépés',
8 | noInternetConnection: 'Nincs internet kapcsolat',
9 | pageNotFound_title: 'Az oldal nem található',
10 | password: 'Jelszó',
11 | projectManagement: 'Projektmenedzsment',
12 | serverConnectionFailed: 'A szerverkapcsolat sikertelen',
13 | unknownError: 'Ismeretlen hiba, próbáld meg később újra',
14 | useSingleSignOn: 'Egyszeri bejelentkezés használata',
15 | },
16 |
17 | action: {
18 | logIn: 'Belépés',
19 | logInWithSSO: 'Belépés SSO-val',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/locales/nl-NL/login.js:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | common: {
4 | emailOrUsername: 'E-mail of gebruikersnaam',
5 | invalidEmailOrUsername: 'Ongeldig e-mailadres of gebruikersnaam',
6 | invalidPassword: 'Ongeldig wachtwoord',
7 | logInToPlanka: 'Inloggen bij Planka',
8 | noInternetConnection: 'Geen internetverbinding',
9 | pageNotFound_title: 'Pagina niet gevonden',
10 | password: 'Wachtwoord',
11 | projectManagement: 'Projectbeheer',
12 | serverConnectionFailed: 'Verbinding met de server mislukt',
13 | unknownError: 'Onbekende fout, probeer het later opnieuw',
14 | useSingleSignOn: 'Gebruik single sign-on',
15 | },
16 |
17 | action: {
18 | logIn: 'Inloggen',
19 | logInWithSSO: 'Inloggen met SSO',
20 | },
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/server/api/responses/unprocessableEntity.js:
--------------------------------------------------------------------------------
1 | /**
2 | * unprocessableEntity.js
3 | *
4 | * A custom response.
5 | *
6 | * Example usage:
7 | * ```
8 | * return res.unprocessableEntity();
9 | * // -or-
10 | * return res.unprocessableEntity(optionalData);
11 | * ```
12 | *
13 | * Or with actions2:
14 | * ```
15 | * exits: {
16 | * somethingHappened: {
17 | * responseType: 'unprocessableEntity'
18 | * }
19 | * }
20 | * ```
21 | *
22 | * ```
23 | * throw 'somethingHappened';
24 | * // -or-
25 | * throw { somethingHappened: optionalData }
26 | * ```
27 | */
28 |
29 | module.exports = function unprocessableEntity(message) {
30 | const { res } = this;
31 |
32 | return res.status(422).json({
33 | code: 'E_UNPROCESSABLE_ENTITY',
34 | message,
35 | });
36 | };
37 |
--------------------------------------------------------------------------------