├── 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 | 2 | 3 | 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 | 2 | 3 | 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 &&