├── .cz.toml ├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── pull_request_template.md ├── taskcafe-full.png ├── taskcafe-full.svg ├── taskcafe.min.svg ├── taskcafe.svg └── taskcafe_preview.png ├── .gitignore ├── .pre-commit-config.yaml ├── .tmuxinator.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── cmd ├── mage │ └── main.go └── taskcafe │ └── main.go ├── conf ├── air.toml └── taskcafe.example.toml ├── docker-compose.dev.yml ├── docker-compose.migrate.yml ├── docker-compose.yml ├── frontend ├── .editorconfig ├── .env ├── .env.development ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.js ├── .storybook │ └── main.js ├── Makefile ├── codegen.yml ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── Admin │ │ └── index.tsx │ ├── App │ │ ├── BaseStyles.ts │ │ ├── NormalizeStyles.ts │ │ ├── Routes.tsx │ │ ├── ThemeStyles.ts │ │ ├── Toast.ts │ │ ├── TopNavbar │ │ │ ├── ProjectFinder.tsx │ │ │ ├── ProjectPopup.tsx │ │ │ └── index.tsx │ │ ├── cache.ts │ │ ├── context.ts │ │ ├── fonts.css │ │ └── index.tsx │ ├── Auth │ │ ├── Styles.ts │ │ └── index.tsx │ ├── Confirm │ │ ├── Styles.ts │ │ └── index.tsx │ ├── Dashboard │ │ └── index.tsx │ ├── MyTasks │ │ ├── MyTasksSort.tsx │ │ ├── MyTasksStatus.tsx │ │ ├── TaskEntry.tsx │ │ └── index.tsx │ ├── Profile │ │ └── index.tsx │ ├── Projects │ │ ├── Project │ │ │ ├── Board │ │ │ │ ├── ControlFilter.tsx │ │ │ │ ├── ControlSort.tsx │ │ │ │ ├── ControlStatus.tsx │ │ │ │ └── index.tsx │ │ │ ├── Details │ │ │ │ └── index.tsx │ │ │ ├── LabelManagerEditor │ │ │ │ └── index.tsx │ │ │ ├── UserManagementPopup │ │ │ │ ├── OptionValue.tsx │ │ │ │ ├── Styles.ts │ │ │ │ ├── UserOption.tsx │ │ │ │ ├── fetchMembers.ts │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Register │ │ ├── Styles.ts │ │ └── index.tsx │ ├── Teams │ │ ├── Members │ │ │ └── index.tsx │ │ ├── Projects │ │ │ └── index.tsx │ │ ├── Settings │ │ │ └── index.tsx │ │ └── index.tsx │ ├── index.tsx │ ├── outline.d.ts │ ├── react-app-env.d.ts │ ├── setupTests.ts │ ├── shared │ │ ├── components │ │ │ ├── AddList │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Admin │ │ │ │ └── index.tsx │ │ │ ├── Button │ │ │ │ └── index.tsx │ │ │ ├── Card │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── CardComposer │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Checklist │ │ │ │ └── index.tsx │ │ │ ├── Chip │ │ │ │ └── index.tsx │ │ │ ├── Confirm │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── ControlledInput │ │ │ │ └── index.tsx │ │ │ ├── DropdownMenu │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── DueDateManager │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── EmptyBoard │ │ │ │ └── index.tsx │ │ │ ├── FormInput │ │ │ │ └── index.tsx │ │ │ ├── Input │ │ │ │ └── index.tsx │ │ │ ├── List │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── ListActions │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Lists │ │ │ │ ├── Styles.ts │ │ │ │ ├── index.tsx │ │ │ │ └── metaFilter.ts │ │ │ ├── LoadingSpinner │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Login │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Member │ │ │ │ └── index.tsx │ │ │ ├── MemberManager │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── MiniProfile │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Modal │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Navbar │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── NewProject │ │ │ │ └── index.tsx │ │ │ ├── NotifcationPopup │ │ │ │ └── index.tsx │ │ │ ├── PopupMenu │ │ │ │ ├── LabelEditor.tsx │ │ │ │ ├── LabelManager.tsx │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── ProfileIcon │ │ │ │ └── index.tsx │ │ │ ├── ProjectGridItem │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── ProjectSettings │ │ │ │ └── index.tsx │ │ │ ├── QuickCardEditor │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Register │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Select │ │ │ │ └── index.tsx │ │ │ ├── Settings │ │ │ │ └── index.tsx │ │ │ ├── Sidebar │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ │ ├── Tabs │ │ │ │ └── index.tsx │ │ │ ├── TaskAssignee │ │ │ │ └── index.tsx │ │ │ ├── TaskDetails │ │ │ │ ├── ActivityMessage.tsx │ │ │ │ ├── CommentCreator.tsx │ │ │ │ ├── Loading.tsx │ │ │ │ ├── Styles.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── onDragEnd.ts │ │ │ │ └── remark.js │ │ │ ├── Textarea │ │ │ │ └── index.tsx │ │ │ └── TopNavbar │ │ │ │ ├── LoggedOut.tsx │ │ │ │ ├── Styles.ts │ │ │ │ └── index.tsx │ │ ├── constants │ │ │ ├── keyCodes.ts │ │ │ ├── labelColors.ts │ │ │ └── menuTypes.ts │ │ ├── fonts │ │ │ ├── OpenSans-Bold.ttf │ │ │ ├── OpenSans-BoldItalic.ttf │ │ │ ├── OpenSans-ExtraBold.ttf │ │ │ ├── OpenSans-ExtraBoldItalic.ttf │ │ │ ├── OpenSans-Italic.ttf │ │ │ ├── OpenSans-Light.ttf │ │ │ ├── OpenSans-LightItalic.ttf │ │ │ ├── OpenSans-Regular.ttf │ │ │ ├── OpenSans-SemiBold.ttf │ │ │ └── OpenSans-SemiBoldItalic.ttf │ │ ├── generated │ │ │ └── graphql.tsx │ │ ├── graphql │ │ │ ├── assignTask.graphqls │ │ │ ├── clearAvatarProfile.graphqls │ │ │ ├── createProject.graphqls │ │ │ ├── createProjectLabel.graphqls │ │ │ ├── createTaskGroup.graphqls │ │ │ ├── deleteProjectLabel.graphqls │ │ │ ├── deleteTask.graphqls │ │ │ ├── deleteTaskGroup.graphqls │ │ │ ├── findProject.ts │ │ │ ├── findTask.graphqls │ │ │ ├── fragments │ │ │ │ └── task.ts │ │ │ ├── getProjects.graphqls │ │ │ ├── labels.ts │ │ │ ├── me.graphqls │ │ │ ├── myTasks.graphqls │ │ │ ├── notificationToggleRead.ts │ │ │ ├── notifications.ts │ │ │ ├── notifictionMarkAllRead.ts │ │ │ ├── onNotificationAdded.ts │ │ │ ├── project │ │ │ │ ├── deleteProject.ts │ │ │ │ ├── deleteProjectInvitedMember.ts │ │ │ │ ├── deleteProjectMember.ts │ │ │ │ ├── inviteProjectMembers.ts │ │ │ │ └── updateProjectMemberRole.ts │ │ │ ├── task │ │ │ │ ├── createTask.ts │ │ │ │ ├── createTaskChecklist.ts │ │ │ │ ├── createTaskChecklistItem.ts │ │ │ │ ├── createTaskComment.ts │ │ │ │ ├── deleteTaskChecklist.ts │ │ │ │ ├── deleteTaskChecklistItem.ts │ │ │ │ ├── deleteTaskComment.ts │ │ │ │ ├── setTaskChecklistItemComplete.ts │ │ │ │ ├── setTaskComplete.ts │ │ │ │ ├── toggleTaskWatcher.ts │ │ │ │ ├── updateTaskChecklistItemLocation.ts │ │ │ │ ├── updateTaskChecklistItemName.ts │ │ │ │ ├── updateTaskChecklistLocation.ts │ │ │ │ ├── updateTaskChecklistName.ts │ │ │ │ └── updateTaskComment.ts │ │ │ ├── taskGroup │ │ │ │ ├── deleteTaskGroupTasks.ts │ │ │ │ ├── duplicateTaskGroup.ts │ │ │ │ ├── sortTaskGroup.ts │ │ │ │ └── updateTaskGroupName.ts │ │ │ ├── team │ │ │ │ ├── createTeam.ts │ │ │ │ ├── createTeamMember.ts │ │ │ │ ├── deleteTeam.ts │ │ │ │ ├── deleteTeamMember.ts │ │ │ │ ├── getTeam.ts │ │ │ │ └── updateTeamMemberRole.ts │ │ │ ├── toggleProjectVisibility.ts │ │ │ ├── toggleTaskLabel.graphqls │ │ │ ├── topNavbar.ts │ │ │ ├── unassignTask.graphqls │ │ │ ├── unreadNotifications.ts │ │ │ ├── updateProjectLabel.graphqls │ │ │ ├── updateProjectName.graphqls │ │ │ ├── updateTaskDescription.graphqls │ │ │ ├── updateTaskDueDate.graphqls │ │ │ ├── updateTaskGroupLocation.graphqls │ │ │ ├── updateTaskLocation.graphqls │ │ │ ├── updateTaskName.graphqls │ │ │ ├── user │ │ │ │ ├── createUser.ts │ │ │ │ ├── deleteInvitedUser.ts │ │ │ │ ├── deleteUser.ts │ │ │ │ ├── updateUserInfo.ts │ │ │ │ ├── updateUserPassword.ts │ │ │ │ └── updateUserRole.ts │ │ │ └── users.graphqls │ │ ├── hooks │ │ │ ├── memoize.ts │ │ │ ├── onEscapeKeyDown.ts │ │ │ ├── onOutsideClick.ts │ │ │ ├── useStateWithLocalStorage.ts │ │ │ ├── useStickyState.ts │ │ │ └── useWindowSize.ts │ │ ├── icons │ │ │ ├── AccountPlus.tsx │ │ │ ├── AngleDown.tsx │ │ │ ├── AngleLeft.tsx │ │ │ ├── ArrowDown.tsx │ │ │ ├── ArrowLeft.tsx │ │ │ ├── At.tsx │ │ │ ├── BarChart.tsx │ │ │ ├── Bell.tsx │ │ │ ├── Bin.tsx │ │ │ ├── Bolt.tsx │ │ │ ├── Briefcase.tsx │ │ │ ├── Bubble.tsx │ │ │ ├── Calendar.tsx │ │ │ ├── CaretDown.tsx │ │ │ ├── CaretRight.tsx │ │ │ ├── CheckCircle.tsx │ │ │ ├── CheckCircleOutline.tsx │ │ │ ├── CheckSquare.tsx │ │ │ ├── CheckSquareOutline.tsx │ │ │ ├── Checkmark.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── Circle.tsx │ │ │ ├── CircleSolid.tsx │ │ │ ├── Clock.tsx │ │ │ ├── Clone.tsx │ │ │ ├── Cog.tsx │ │ │ ├── Cogs.tsx │ │ │ ├── Cross.tsx │ │ │ ├── Crown.tsx │ │ │ ├── Dot.tsx │ │ │ ├── DotCircle.tsx │ │ │ ├── DoubleChevronUp.tsx │ │ │ ├── Ellipsis.tsx │ │ │ ├── Exit.tsx │ │ │ ├── Eye.tsx │ │ │ ├── EyeSlash.tsx │ │ │ ├── Filter.tsx │ │ │ ├── Home.tsx │ │ │ ├── Icon.tsx │ │ │ ├── List.tsx │ │ │ ├── ListUnordered.tsx │ │ │ ├── Lock.tsx │ │ │ ├── Paperclip.tsx │ │ │ ├── Pencil.tsx │ │ │ ├── Plus.tsx │ │ │ ├── Question.tsx │ │ │ ├── Share.tsx │ │ │ ├── Smile.tsx │ │ │ ├── Sort.tsx │ │ │ ├── Square.tsx │ │ │ ├── Stack.tsx │ │ │ ├── Star.tsx │ │ │ ├── Tags.tsx │ │ │ ├── Task.tsx │ │ │ ├── Taskcafe.tsx │ │ │ ├── ToggleOn.tsx │ │ │ ├── Trash.tsx │ │ │ ├── User.tsx │ │ │ ├── UserCircle.tsx │ │ │ ├── UserPlus.tsx │ │ │ ├── Users.tsx │ │ │ └── index.ts │ │ ├── undraw │ │ │ ├── AccessAccount.tsx │ │ │ ├── Empty.tsx │ │ │ └── NoData.tsx │ │ └── utils │ │ │ ├── boundingRect.ts │ │ │ ├── cache.ts │ │ │ ├── draggables.ts │ │ │ ├── editorTheme.ts │ │ │ ├── email.ts │ │ │ ├── localStorage.ts │ │ │ ├── noop.ts │ │ │ ├── polling.ts │ │ │ ├── sorting.ts │ │ │ └── styles.ts │ ├── styled.d.ts │ ├── taskcafe.d.ts │ └── types.d.ts ├── tsconfig.json └── yarn.lock ├── go.mod ├── go.sum ├── gqlgen.yml ├── internal ├── commands │ ├── commands.go │ ├── commands_prod.go │ ├── job.go │ ├── migrate.go │ ├── reset_password.go │ ├── seed.go │ ├── token.go │ ├── web.go │ └── worker.go ├── config │ └── config.go ├── db │ ├── db.go │ ├── label_color.sql.go │ ├── models.go │ ├── notification.sql.go │ ├── organization.sql.go │ ├── project.sql.go │ ├── project_label.sql.go │ ├── querier.go │ ├── query │ │ ├── label_color.sql │ │ ├── notification.sql │ │ ├── organization.sql │ │ ├── project.sql │ │ ├── project_label.sql │ │ ├── system_options.sql │ │ ├── task.sql │ │ ├── task_activity.sql │ │ ├── task_assigned.sql │ │ ├── task_checklist.sql │ │ ├── task_group.sql │ │ ├── task_label.sql │ │ ├── team.sql │ │ ├── team_member.sql │ │ ├── token.sql │ │ └── user_accounts.sql │ ├── repository.go │ ├── system_options.sql.go │ ├── task.sql.go │ ├── task_activity.sql.go │ ├── task_assigned.sql.go │ ├── task_checklist.sql.go │ ├── task_group.sql.go │ ├── task_label.sql.go │ ├── team.sql.go │ ├── team_member.sql.go │ ├── token.sql.go │ └── user_accounts.sql.go ├── graph │ ├── generated.go │ ├── graph.go │ ├── helpers.go │ ├── models_gen.go │ ├── notification.resolvers.go │ ├── project.resolvers.go │ ├── resolver.go │ ├── scalars.go │ ├── schema.resolvers.go │ ├── schema │ │ ├── notification.gql │ │ ├── notification │ │ │ └── notification.gql │ │ ├── project.gql │ │ ├── project │ │ │ ├── _model.gql │ │ │ ├── label.gql │ │ │ ├── member.gql │ │ │ └── project.gql │ │ ├── schema.gql │ │ ├── task.gql │ │ ├── task │ │ │ ├── _model.gql │ │ │ ├── checklist.gql │ │ │ ├── comment.gql │ │ │ ├── group.gql │ │ │ ├── label.gql │ │ │ └── task.gql │ │ ├── taskList.gql │ │ ├── taskList │ │ │ ├── _model.gql │ │ │ └── taskList.gql │ │ ├── team.gql │ │ ├── team │ │ │ ├── _model.gql │ │ │ ├── member.gql │ │ │ └── team.gql │ │ ├── user.gql │ │ └── user │ │ │ ├── _model.gql │ │ │ └── user.gql │ ├── task.resolvers.go │ ├── taskList.resolvers.go │ ├── team.resolvers.go │ └── user.resolvers.go ├── jobs │ ├── jobs.go │ └── logger.go ├── logger │ ├── logger.go │ └── route_logger.go ├── route │ ├── auth.go │ ├── avatar.go │ ├── log.go │ ├── middleware.go │ ├── route.go │ └── settings.go └── utils │ ├── context.go │ ├── cursor.go │ ├── mail.go │ ├── redis.go │ └── version.go ├── magefile.go ├── migrations ├── 0001_add-refresh-token-table.up.sql ├── 0002_add-user_account-table.up.sql ├── 0003_add-team-table.up.sql ├── 0004_add-project-table.up.sql ├── 0005_add-task-group-table.up.sql ├── 0006_add-task.up.sql ├── 0007_add-organization-table.up.sql ├── 0008_add-org-id-to-team-table.up.sql ├── 0009_add-task-assigned-table.up.sql ├── 0010_add-description-to-task-table.up.sql ├── 0011_add-label-color-table.up.sql ├── 0012_add-project-label-table.up.sql ├── 0013_add-due-date-to-task-table.up.sql ├── 0014_add-owner-column-to-project-table.up.sql ├── 0015_add-task-label-table.up.sql ├── 0016_add-profile_bg_color-column-to-user-account-table.up.sql ├── 0017_add-profile-bg-delete-cascade.up.sql ├── 0018_add-name-column-to-label-color.up.sql ├── 0019_add-unique-constraint-project-label-and-task-on-task-label.up.sql ├── 0020_add-full-name-column-to-user_account-table.up.sql ├── 0021_drop-first-name-column-from-user_account-table.up.sql ├── 0022_drop-last-name-column-from-user_account-table.up.sql ├── 0023_add-initials-column-to-user_account-table.up.sql ├── 0024_add-profile-avatar-url-column-to-user_account-table.up.sql ├── 0025_add-cascade-delete-to-task-id-fk-on-task_label.up.sql ├── 0026_add-cascade-delete-to-task-id-fk-on-task_assigned.up.sql ├── 0027_add-cascade-delete-to-task_group_id-fk-on-task.up.sql ├── 0028_add-complete-column-to-task-table.up.sql ├── 0029_add-task_checklist.up.sql ├── 0030_add-task_checklist_item.up.sql ├── 0031_add-team-member-table.up.sql ├── 0032_add-cascade-delete-to-project_label.up.sql ├── 0033_add-cascade-delete-to_task_label_project_label_id_fkey.up.sql ├── 0034_add-cascade-delete-to_project_team_id_fkey.up.sql ├── 0035_add-cascade-delete-to-task_assigned_user_id_fkey.up.sql ├── 0036_add-fkey-to-user_id-on-refresh_token-table.up.sql ├── 0037_add-project_member-table.up.sql ├── 0038_add-role-table.up.sql ├── 0039_add-role-column-to-user_account-table.up.sql ├── 0040_add-unique-constraint-to-project_member-table.up.sql ├── 0041_add-role_code-column-to-project_member-table.up.sql ├── 0042_add-default-null-to-profile_avatar_url-on-user_account.up.sql ├── 0043_add-role_code-column-to-team_member-table.up.sql ├── 0044_add-owner-to-team-table.up.sql ├── 0045_add-system_options-table.up.sql ├── 0046_add-system-user.up.sql ├── 0047_add-default-label_colors.up.sql ├── 0048_add-cascade-delete-to-task_group_project_id_fkey.up.sql ├── 0049_remove-owner-column-from-project-table.up.sql ├── 0050_remove-owner-column-from-team-table.up.sql ├── 0051_add-completed_at-to-task-table.up.sql ├── 0052_add-bio-col-to-user_account.up.sql ├── 0053_add-notification-tables.up.sql ├── 0054_remove-team-not-null-constraint-projects.up.sql ├── 0055_add-personal_project_table.up.sql ├── 0056_add-user_account_invited-table.up.sql ├── 0057_add-project_member_invited-table.up.sql ├── 0058_add-active-column-to-user_account.up.sql ├── 0059_add-confirm_token_table.up.sql ├── 0060_add-task_activity-table.up.sql ├── 0061_add-task_comment-table.up.sql ├── 0062_add-cascade-delete-to-task-comments-and-activity.up.sql ├── 0063_add-use_time-to-task-due_date.up.sql ├── 0064_rename-refresh_token-to-auth_token.up.sql ├── 0065_add-public_on-to-project.up.sql ├── 0066_redesign-notification-table.up.sql ├── 0067_add-user_account_settings.up.sql ├── 0068_add-task_watcher-table.up.sql ├── 0069_add-short_id-to-tasks-and-projects.up.sql ├── 0070_add-task_due_date_notification.up.sql ├── 0071_add-task_due_date_notification-at-col.up.sql └── 0072_remove-not-null-from-notification-caused_by.up.sql ├── scripts └── lint.sh ├── sqlc.yaml ├── templates └── mail │ └── user │ └── registered.tmpl ├── testing ├── docker-compose.dev.yml └── docker-compose.latest.yml └── uploads └── .keep /.cz.toml: -------------------------------------------------------------------------------- 1 | [tool.commitizen] 2 | name = "cz_conventional_commits" 3 | version = "0.0.1" 4 | tag_format = "$version" 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | dist 3 | uploads 4 | frontend/node_modules 5 | internal/frontend/frontend_generated.go 6 | internal/migrations/migrations_generated.go 7 | taskcafe 8 | conf/taskcafe.toml 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://paypal.me/jordanthedev', 'https://www.buymeacoffee.com/jordanknott'] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve Taskcafe 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is with steps to reproduce the issue. 11 | 12 | **Expected behavior** 13 | What did you expect to happen? 14 | 15 | **Screenshots / Live demo link** 16 | If applicable, add screenshots to help explain your problem. 17 | 18 | **Additional context** 19 | Add any other context about the problem here. 20 | 21 | Please send the Taskcafe web service logs if applicable. 22 | 23 | 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Request 4 | url: https://github.com/JordanKnott/taskcafe/discussions/new?category=ideas 5 | about: Share ideas for new features 6 | - name: Ask a Question 7 | url: https://github.com/JordanKnott/taskcafe/discussions/new?category=q-a 8 | about: Ask the community for help 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | * **Please check if the PR fulfills these requirements** 2 | - [ ] You have read the contribution guidelines [guidelines](https://github.com/JordanKnott/taskcafe/blob/master/CONTRIBUTING.md) 3 | - [ ] The commit message follows our [guidelines](https://github.com/JordanKnott/taskcafe/blob/master/CONTRIBUTING.md#git-commit-message-style) 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | 6 | 7 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 8 | 9 | 10 | * **What is the current behavior?** (You can also link to an open issue here) 11 | 12 | 13 | * **What is the new behavior (if this is a feature change)?** 14 | 15 | 16 | * **Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) 17 | 18 | 19 | * **Other information**: 20 | -------------------------------------------------------------------------------- /.github/taskcafe-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/.github/taskcafe-full.png -------------------------------------------------------------------------------- /.github/taskcafe_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/.github/taskcafe_preview.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | uploads/* 4 | !uploads/.keep 5 | internal/frontend/frontend_generated.go 6 | internal/migrations/migrations_generated.go 7 | taskcafe 8 | conf/taskcafe.toml 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: eslint 5 | name: eslint 6 | entry: scripts/lint.sh 7 | language: system 8 | files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx 9 | types: [file] 10 | - hooks: 11 | - id: check-yaml 12 | - id: end-of-file-fixer 13 | - id: trailing-whitespace 14 | repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v2.3.0 16 | - hooks: 17 | - id: go-fmt 18 | - id: go-lint 19 | - id: go-imports 20 | - id: go-unit-tests 21 | - id: go-build 22 | - id: go-mod-tidy 23 | repo: git://github.com/dnephin/pre-commit-golang 24 | rev: master 25 | - hooks: 26 | - id: commitizen 27 | stages: [commit-msg] 28 | repo: https://github.com/commitizen-tools/commitizen 29 | rev: master 30 | -------------------------------------------------------------------------------- /.tmuxinator.yml: -------------------------------------------------------------------------------- 1 | name: taskcafe 2 | root: . 3 | 4 | windows: 5 | - services: 6 | root: ./ 7 | panes: 8 | - api: 9 | - go run cmd/taskcafe/main.go web 10 | - yarn: 11 | - cd frontend 12 | - yarn start 13 | - worker: 14 | - go run cmd/taskcafe/main.go worker 15 | - web/editor: 16 | root: ./frontend 17 | panes: 18 | - vim src/index.tsx 19 | - api/editor: 20 | root: ./ 21 | panes: 22 | - vim cmd/taskcafe/main.go 23 | - database: 24 | root: ./ 25 | panes: 26 | - pgcli postgres://taskcafe:taskcafe_test@localhost:8855/taskcafe 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## UNRELEASED 8 | 9 | ### Added 10 | - On login page, redirects to `/register` if no users exist (to help streamline initial setup) 11 | 12 | ### Fixed 13 | - Fixes new user popup form so that it can now be submitted 14 | 15 | ## [0.3.5] - 2021-09-04 16 | 17 | ### Added 18 | - Project visibility can now be set to public - meaning anyone can view the project board 19 | - When redirected to login page while trying to view a page that requires login, you'll be redirected back to the correct page after login 20 | - When creating a new label within the LabelManager on a card, the new label will automatically be applied to the task after creation 21 | 22 | ### Changed 23 | - Switch primary font to Open Sans 24 | 25 | ### Fixed 26 | - Any open popups are hidden when closing the Task Details window 27 | 28 | ## [0.1.1] - 2020-08-21 29 | 30 | ### Fixed 31 | - fix panic(nil) when loading config if config file actually exists 32 | 33 | ## [0.1.0] - 2020-08-21 34 | 35 | ### Added 36 | - first "stable" alpha release 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.5-alpine as frontend 2 | RUN apk --no-cache add curl 3 | WORKDIR /usr/src/app 4 | COPY frontend . 5 | RUN yarn install 6 | RUN yarn build 7 | 8 | FROM golang:1.14.5-alpine as backend 9 | WORKDIR /usr/src/app 10 | COPY go.mod go.mod 11 | COPY go.sum go.sum 12 | RUN go mod download 13 | COPY . . 14 | COPY --from=frontend /usr/src/app/build ./frontend/build 15 | RUN go run cmd/mage/main.go backend:genFrontend backend:genMigrations backend:build 16 | 17 | FROM alpine:latest 18 | WORKDIR /root/ 19 | COPY --from=backend /usr/src/app/dist/taskcafe . 20 | CMD ["./taskcafe", "web"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | pre-commit = "*" 10 | 11 | [requires] 12 | python_version = "3.9" 13 | -------------------------------------------------------------------------------- /cmd/mage/main.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/magefile/mage/mage" 9 | ) 10 | 11 | func main() { os.Exit(mage.Main()) } 12 | -------------------------------------------------------------------------------- /cmd/taskcafe/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jordanknott/taskcafe/internal/commands" 5 | _ "github.com/lib/pq" 6 | ) 7 | 8 | func main() { 9 | commands.Execute() 10 | } 11 | -------------------------------------------------------------------------------- /conf/air.toml: -------------------------------------------------------------------------------- 1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format 2 | 3 | # Working directory 4 | # . or absolute path, please note that the directories following must be under root. 5 | root = "." 6 | tmp_dir = "tmp" 7 | 8 | [build] 9 | # Just plain old shell command. You could use `make` as well. 10 | cmd = "go build -o ./dist/taskcafe cmd/taskcafe/main.go" 11 | # Binary file yields from `cmd`. 12 | bin = "dist/taskcafe" 13 | # Customize binary. 14 | full_bin = "./dist/taskcafe web" 15 | # Watch these filename extensions. 16 | include_ext = ["go"] 17 | # Ignore these filename extensions or directories. 18 | exclude_dir = ["dist", "frontend"] 19 | # Watch these directories if you specified. 20 | include_dir = [] 21 | # Exclude files. 22 | exclude_file = [] 23 | # This log file places in your tmp_dir. 24 | log = "air.log" 25 | # It's not necessary to trigger build each time file changes if it's too frequent. 26 | delay = 1000 # ms 27 | # Stop running old binary when build errors occur. 28 | stop_on_error = true 29 | # Send Interrupt signal before killing process (windows does not support this feature) 30 | send_interrupt = false 31 | # Delay after sending Interrupt signal 32 | kill_delay = 500 # ms 33 | 34 | [log] 35 | # Show log time 36 | time = false 37 | 38 | [color] 39 | # Customize each part's color. If no color found, use the raw app log. 40 | main = "magenta" 41 | watcher = "cyan" 42 | build = "yellow" 43 | runner = "green" 44 | 45 | [misc] 46 | # Delete tmp directory on exit 47 | clean_on_exit = true 48 | -------------------------------------------------------------------------------- /conf/taskcafe.example.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | hostname = '0.0.0.0:3333' 3 | 4 | [email_notifications] 5 | enabled = true 6 | display_name = "No Reply" 7 | from_address = "example.com" 8 | 9 | [storage] 10 | storage_system = 'local_storage' 11 | upload_dir_path = 'uploads' 12 | 13 | [database] 14 | host = 'postgres' 15 | name = 'taskcafe' 16 | user = 'taskcafe' 17 | password = 'taskcafe_test' 18 | 19 | [smtp] 20 | username = 'taskcafe@example.com' 21 | password = '' 22 | from = 'no-reply@taskcafe.com' 23 | host = 'localhost' 24 | port = 11500 25 | skip_verify = false 26 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | postgres: 4 | image: postgres:12.3-alpine 5 | restart: always 6 | networks: 7 | - taskcafe-test 8 | environment: 9 | POSTGRES_USER: taskcafe 10 | POSTGRES_PASSWORD: taskcafe_test 11 | POSTGRES_DB: taskcafe 12 | volumes: 13 | - taskcafe-postgres:/var/lib/postgresql/data 14 | ports: 15 | - 8865:5432 16 | mailhog: 17 | image: mailhog/mailhog:latest 18 | restart: always 19 | ports: 20 | - 1025:1025 21 | - 8025:8025 22 | redis: 23 | image: redis:6.2 24 | restart: always 25 | ports: 26 | - 6379:6379 27 | 28 | volumes: 29 | taskcafe-postgres: 30 | external: false 31 | 32 | networks: 33 | taskcafe-test: 34 | driver: bridge 35 | -------------------------------------------------------------------------------- /docker-compose.migrate.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | migrate: 5 | build: . 6 | entrypoint: ./taskcafe migrate 7 | volumes: 8 | - ./migrations:/root/migrations 9 | depends_on: 10 | - postgres 11 | networks: 12 | - taskcafe-test 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | image: taskcafe/taskcafe:latest 5 | # build: . 6 | ports: 7 | - "3333:3333" 8 | depends_on: 9 | - postgres 10 | networks: 11 | - taskcafe-test 12 | environment: 13 | TASKCAFE_DATABASE_HOST: postgres 14 | TASKCAFE_MIGRATE: "true" 15 | volumes: 16 | - taskcafe-uploads:/root/uploads 17 | 18 | postgres: 19 | image: postgres:12.3-alpine 20 | restart: always 21 | networks: 22 | - taskcafe-test 23 | environment: 24 | POSTGRES_USER: taskcafe 25 | POSTGRES_PASSWORD: taskcafe_test 26 | POSTGRES_DB: taskcafe 27 | volumes: 28 | - taskcafe-postgres:/var/lib/postgresql/data 29 | 30 | volumes: 31 | taskcafe-postgres: 32 | external: false 33 | taskcafe-uploads: 34 | external: false 35 | 36 | networks: 37 | taskcafe-test: 38 | driver: bridge 39 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_ENABLE_POLLING=true 2 | ESLINT_NO_DEV_ERRORS=true 3 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_ENABLE_POLLING=false 2 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | src/shared/generated/*.tsx 2 | src/shared/generated/*.ts 3 | src/react-app-env.d.ts 4 | -------------------------------------------------------------------------------- /frontend/.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 | report* 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | bracketSpacing: true, 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | stories: ['../src/shared/components/**/*.stories.tsx'], 5 | addons: [ 6 | '@storybook/addon-actions/register', 7 | '@storybook/addon-links', 8 | '@storybook/addon-storysource', 9 | '@storybook/addon-knobs/register', 10 | '@storybook/addon-docs/register', 11 | '@storybook/addon-viewport/register', 12 | '@storybook/addon-backgrounds/register', 13 | ], 14 | webpackFinal: async config => { 15 | config.resolve.modules.push(path.resolve(__dirname, '../src')); 16 | return config; 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/Makefile: -------------------------------------------------------------------------------- 1 | 2 | start: 3 | yarn start 4 | -------------------------------------------------------------------------------- /frontend/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 3 | - '../internal/graph/schema/*.gql' 4 | documents: 5 | - 'src/shared/graphql/*.graphqls' 6 | - 'src/shared/graphql/**/*.ts' 7 | generates: 8 | src/shared/generated/graphql.tsx: 9 | plugins: 10 | - 'typescript' 11 | - 'typescript-operations' 12 | - 'typescript-react-apollo' 13 | config: 14 | withHOC: false 15 | withComponent: false 16 | withHooks: true 17 | scalars: 18 | UUID: string 19 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App/ThemeStyles.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | import Color from 'color'; 3 | 4 | const theme: DefaultTheme = { 5 | borderRadius: { 6 | primary: '3x', 7 | alternate: '6px', 8 | }, 9 | colors: { 10 | multiColors: ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'], 11 | primary: 'rgb(115, 103, 240)', 12 | secondary: 'rgb(216, 93, 216)', 13 | alternate: 'rgb(65, 69, 97)', 14 | success: 'rgb(40, 199, 111)', 15 | danger: 'rgb(234, 84, 85)', 16 | warning: 'rgb(255, 159, 67)', 17 | dark: 'rgb(30, 30, 30)', 18 | text: { 19 | primary: 'rgb(194, 198, 220)', 20 | secondary: 'rgb(255, 255, 255)', 21 | }, 22 | border: 'rgb(65, 69, 97)', 23 | bg: { 24 | primary: 'rgb(16, 22, 58)', 25 | secondary: 'rgb(38, 44, 73)', 26 | }, 27 | }, 28 | }; 29 | 30 | export default theme; 31 | -------------------------------------------------------------------------------- /frontend/src/App/Toast.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ToastContainer } from 'react-toastify'; 3 | 4 | const ToastedContainer = styled(ToastContainer).attrs({ 5 | // custom props 6 | })` 7 | .Toastify__toast-container { 8 | } 9 | .Toastify__toast { 10 | padding: 5px; 11 | margin-left: 5px; 12 | margin-right: 5px; 13 | border-radius: 10px; 14 | background: #7367f0; 15 | color: #fff; 16 | } 17 | .Toastify__toast--error { 18 | background: ${props => props.theme.colors.danger}; 19 | } 20 | .Toastify__toast--warning { 21 | background: ${props => props.theme.colors.warning}; 22 | } 23 | .Toastify__toast--success { 24 | background: ${props => props.theme.colors.success}; 25 | } 26 | .Toastify__toast-body { 27 | } 28 | .Toastify__progress-bar { 29 | } 30 | .Toastify__close-button { 31 | display: none; 32 | } 33 | `; 34 | 35 | export default ToastedContainer; 36 | -------------------------------------------------------------------------------- /frontend/src/App/cache.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryCache } from '@apollo/client'; 2 | 3 | const cache = new InMemoryCache(); 4 | 5 | export default cache; 6 | -------------------------------------------------------------------------------- /frontend/src/App/context.ts: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | type UserContextState = { 4 | user: string | null; 5 | setUser: (user: string | null) => void; 6 | }; 7 | 8 | export const UserContext = React.createContext({ 9 | user: null, 10 | setUser: _user => null, 11 | }); 12 | 13 | export const useCurrentUser = () => { 14 | const { user, setUser } = useContext(UserContext); 15 | return { 16 | user, 17 | setUser, 18 | }; 19 | }; 20 | 21 | export default UserContext; 22 | -------------------------------------------------------------------------------- /frontend/src/App/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | src: url(../shared/fonts/OpenSans-Regular.ttf) format('truetype'); 4 | /* other formats include: 'woff2', 'truetype, 'opentype', 5 | 'embedded-opentype', and 'svg' */ 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { PopupProvider } from 'shared/components/PopupMenu'; 4 | import styled, { ThemeProvider } from 'styled-components'; 5 | import NormalizeStyles from './NormalizeStyles'; 6 | import BaseStyles from './BaseStyles'; 7 | import theme from './ThemeStyles'; 8 | import Routes from './Routes'; 9 | import ToastedContainer from './Toast'; 10 | import { UserContext } from './context'; 11 | 12 | import 'react-toastify/dist/ReactToastify.css'; 13 | import './fonts.css'; 14 | 15 | const App = () => { 16 | const [user, setUser] = useState(null); 17 | 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /frontend/src/Auth/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | height: 100vh; 8 | 9 | @media (max-width: 600px) { 10 | position: relative; 11 | top: 30%; 12 | font-size: 150px; 13 | } 14 | `; 15 | 16 | export const LoginWrapper = styled.div` 17 | width: 70%; 18 | 19 | @media (max-width: 600px) { 20 | width: 90%; 21 | margin-top: 50vh; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /frontend/src/Confirm/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100vw; 8 | height: 100vh; 9 | `; 10 | 11 | export const LoginWrapper = styled.div` 12 | width: 60%; 13 | `; 14 | -------------------------------------------------------------------------------- /frontend/src/Confirm/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Confirm from 'shared/components/Confirm'; 3 | import { useHistory, useLocation } from 'react-router'; 4 | import * as QueryString from 'query-string'; 5 | import { useCurrentUser } from 'App/context'; 6 | import { Container, LoginWrapper } from './Styles'; 7 | 8 | const UsersConfirm = () => { 9 | const history = useHistory(); 10 | const location = useLocation(); 11 | const params = QueryString.parse(location.search); 12 | const [hasFailed, setFailed] = useState(false); 13 | const { setUser } = useCurrentUser(); 14 | useEffect(() => { 15 | fetch('/auth/confirm', { 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | confirmToken: params.confirmToken, 19 | }), 20 | }) 21 | .then(async (x) => { 22 | const { status } = x; 23 | if (status === 200) { 24 | const response = await x.json(); 25 | const { userID } = response; 26 | setUser(userID); 27 | history.push('/'); 28 | } else { 29 | setFailed(true); 30 | } 31 | }) 32 | .catch(() => { 33 | setFailed(false); 34 | }); 35 | }, []); 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default UsersConfirm; 46 | -------------------------------------------------------------------------------- /frontend/src/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from 'react-router'; 3 | 4 | const Dashboard: React.FC = () => { 5 | return ; 6 | }; 7 | 8 | export default Dashboard; 9 | -------------------------------------------------------------------------------- /frontend/src/Projects/Project/UserManagementPopup/OptionValue.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Cross } from 'shared/icons'; 3 | import * as S from './Styles'; 4 | 5 | const OptionValue = ({ data, removeProps }: any) => { 6 | return ( 7 | 8 | {data.label} 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default OptionValue; 17 | -------------------------------------------------------------------------------- /frontend/src/Projects/Project/UserManagementPopup/UserOption.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TaskAssignee from 'shared/components/TaskAssignee'; 3 | import * as S from './Styles'; 4 | 5 | type UserOptionProps = { 6 | innerProps: any; 7 | isDisabled: boolean; 8 | isFocused: boolean; 9 | label: string; 10 | data: any; 11 | getValue: any; 12 | }; 13 | 14 | const UserOption: React.FC = ({ isDisabled, isFocused, innerProps, label, data }) => { 15 | return !isDisabled ? ( 16 | 17 | 25 | 26 | 27 | {label} 28 | 29 | {data.value.type === 2 && ( 30 | 31 | Joined 32 | 33 | )} 34 | 35 | 36 | ) : null; 37 | }; 38 | 39 | export default UserOption; 40 | -------------------------------------------------------------------------------- /frontend/src/Register/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100vw; 8 | height: 100vh; 9 | `; 10 | 11 | export const LoginWrapper = styled.div` 12 | width: 60%; 13 | `; 14 | -------------------------------------------------------------------------------- /frontend/src/Teams/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TeamSettings = () => { 4 | return

HI!

; 5 | }; 6 | 7 | export default TeamSettings; 8 | -------------------------------------------------------------------------------- /frontend/src/outline.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/outline.d.ts -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /frontend/src/shared/components/CardComposer/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Button from 'shared/components/Button'; 3 | import TextareaAutosize from 'react-autosize-textarea'; 4 | import { mixin } from 'shared/utils/styles'; 5 | 6 | export const CancelIconWrapper = styled.div` 7 | opacity: 0.8; 8 | cursor: pointer; 9 | font-size: 1.25em; 10 | padding-left: 5px; 11 | `; 12 | 13 | export const CardComposerWrapper = styled.div<{ isOpen: boolean }>` 14 | padding-bottom: 8px; 15 | display: ${props => (props.isOpen ? 'flex' : 'none')}; 16 | flex-direction: column; 17 | `; 18 | 19 | export const ComposerControls = styled.div``; 20 | 21 | export const ComposerControlsSaveSection = styled.div` 22 | display: flex; 23 | float: left; 24 | align-items: center; 25 | justify-content: center; 26 | `; 27 | export const ComposerControlsActionsSection = styled.div` 28 | float: right; 29 | `; 30 | export const AddCardButton = styled(Button)` 31 | margin-right: 4px; 32 | padding: 6px 12px; 33 | border-radius: 3px; 34 | `; 35 | -------------------------------------------------------------------------------- /frontend/src/shared/components/ListActions/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ListActionsWrapper = styled.ul` 4 | list-style-type: none; 5 | margin: 0; 6 | padding: 0; 7 | `; 8 | 9 | export const ListActionItemWrapper = styled.li` 10 | margin: 0; 11 | padding: 0; 12 | `; 13 | export const ListActionItem = styled.span` 14 | cursor: pointer; 15 | display: block; 16 | font-size: 14px; 17 | color: #c2c6dc; 18 | font-weight: 400; 19 | padding: 6px 12px; 20 | position: relative; 21 | margin: 0 -12px; 22 | text-decoration: none; 23 | &:hover { 24 | background: ${props => props.theme.colors.primary}; 25 | } 26 | `; 27 | 28 | export const ListSeparator = styled.hr` 29 | background-color: #414561; 30 | border: 0; 31 | height: 1px; 32 | margin: 8px 0; 33 | padding: 0; 34 | width: 100%; 35 | `; 36 | 37 | export const InnerContent = styled.div` 38 | margin: 0 12px; 39 | `; 40 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Lists/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | user-select: none; 5 | white-space: nowrap; 6 | 7 | ::-webkit-scrollbar { 8 | height: 10px; 9 | } 10 | ::-webkit-scrollbar-thumb { 11 | background: #7367f0; 12 | border-radius: 6px; 13 | } 14 | 15 | ::-webkit-scrollbar-track { 16 | background: #10163a; 17 | border-radius: 6px; 18 | } 19 | `; 20 | 21 | export const BoardContainer = styled.div` 22 | position: relative; 23 | overflow-y: auto; 24 | outline: none; 25 | flex-grow: 1; 26 | `; 27 | 28 | export const BoardWrapper = styled.div` 29 | display: flex; 30 | 31 | user-select: none; 32 | white-space: nowrap; 33 | overflow-x: auto; 34 | overflow-y: hidden; 35 | padding-bottom: 4px; 36 | position: absolute; 37 | top: 0; 38 | right: 0; 39 | bottom: 0; 40 | left: 0; 41 | `; 42 | export default Container; 43 | -------------------------------------------------------------------------------- /frontend/src/shared/components/LoadingSpinner/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const LoadingSpinnerKeyframes = keyframes` 4 | 0% { 5 | transform: rotate(0deg); 6 | } 7 | 100% { 8 | transform: rotate(360deg); 9 | } 10 | `; 11 | 12 | export const LoadingSpinnerWrapper = styled.div<{ color: string; size: string; borderSize: string; thickness: string }>` 13 | display: inline-block; 14 | position: relative; 15 | width: ${props => props.borderSize}; 16 | height: ${props => props.borderSize}; 17 | 18 | & > div { 19 | box-sizing: border-box; 20 | display: block; 21 | position: absolute; 22 | width: ${props => props.size}; 23 | height: ${props => props.size}; 24 | margin: ${props => props.thickness}; 25 | border: ${props => props.thickness} solid ${props => props.theme.colors[props.color]}; 26 | border-radius: 50%; 27 | animation: 1.2s ${LoadingSpinnerKeyframes} cubic-bezier(0.5, 0, 0.5, 1) infinite; 28 | border-color: ${props => props.theme.colors[props.color]} transparent transparent transparent; 29 | } 30 | 31 | & > div:nth-child(1) { 32 | animation-delay: -0.45s; 33 | } 34 | & > div:nth-child(2) { 35 | animation-delay: -0.3s; 36 | } 37 | & > div:nth-child(3) { 38 | animation-delay: -0.15s; 39 | } 40 | `; 41 | 42 | export default LoadingSpinnerWrapper; 43 | -------------------------------------------------------------------------------- /frontend/src/shared/components/LoadingSpinner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { LoadingSpinnerWrapper } from './Styles'; 4 | 5 | type LoadingSpinnerProps = { 6 | color?: 'primary' | 'danger' | 'success' | 'warning' | 'dark'; 7 | size?: string; 8 | borderSize?: string; 9 | thickness?: string; 10 | }; 11 | 12 | /** 13 | * The default parameters may not be applicable to every scenario 14 | * 15 | * While borderSize and size should be a single prop, 16 | * it is currently not as such because it would require math to be done to strings 17 | * e.g "80px - 16" 18 | * 19 | * 20 | * @param color 21 | * @param size The size of the spinner. It is recommended to be at least 16 px less than the borderSize 22 | * @param thickness 23 | * @param borderSize Border size affects the size of the border which if is too small may break the spinner. 24 | * @constructor 25 | */ 26 | const LoadingSpinner: React.FC = ({ 27 | color = 'primary', 28 | size = '64px', 29 | thickness = '8px', 30 | borderSize = '80px', 31 | }) => { 32 | return ( 33 | 34 |
35 |
36 |
37 | 38 | ); 39 | }; 40 | 41 | export default LoadingSpinner; 42 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Modal/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { mixin } from 'shared/utils/styles'; 3 | 4 | export const ScrollOverlay = styled.div` 5 | z-index: 3000; 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | height: 100%; 10 | width: 100%; 11 | overflow-x: hidden; 12 | overflow-y: auto; 13 | `; 14 | 15 | export const ClickableOverlay = styled.div` 16 | min-height: 100%; 17 | background: rgba(0, 0, 0, 0.4); 18 | `; 19 | 20 | export const StyledModal = styled.div<{ width: number; height: number }>` 21 | position: relative; 22 | width: ${props => props.width}px; 23 | height: ${props => props.height}px; 24 | left: 0; 25 | right: 0; 26 | top: 48px; 27 | bottom: 16px; 28 | margin: auto; 29 | 30 | background: #262c49; 31 | vertical-align: middle; 32 | border-radius: 6px; 33 | ${mixin.boxShadowMedium} 34 | `; 35 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Taskcafe } from 'shared/icons'; 3 | import { 4 | Container, 5 | LogoWrapper, 6 | IconWrapper, 7 | Logo, 8 | LogoTitle, 9 | ActionContainer, 10 | ActionButtonContainer, 11 | ActionButtonWrapper, 12 | ActionButtonTitle, 13 | } from './Styles'; 14 | 15 | type ActionButtonProps = { 16 | name: string; 17 | active?: boolean; 18 | }; 19 | 20 | export const ActionButton: React.FC = ({ name, active, children }) => { 21 | return ( 22 | 23 | {children} 24 | {name} 25 | 26 | ); 27 | }; 28 | 29 | export const ButtonContainer: React.FC = ({ children }) => ( 30 | 31 | {children} 32 | 33 | ); 34 | 35 | export const PrimaryLogo = () => { 36 | return ( 37 | 38 | 39 | Taskcafé 40 | 41 | ); 42 | }; 43 | 44 | const Navbar: React.FC = ({ children }) => { 45 | return {children}; 46 | }; 47 | 48 | export default Navbar; 49 | -------------------------------------------------------------------------------- /frontend/src/shared/components/ProfileIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>` 5 | width: ${(props) => props.size}px; 6 | height: ${(props) => props.size}px; 7 | border-radius: 9999px; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | color: #fff; 12 | font-weight: 700; 13 | background: ${(props) => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)}; 14 | background-position: center; 15 | background-size: contain; 16 | `; 17 | 18 | type ProfileIconProps = { 19 | user: TaskUser; 20 | onProfileClick: ($target: React.RefObject, user: TaskUser) => void; 21 | size: number | string; 22 | }; 23 | 24 | const ProfileIcon: React.FC = ({ user, onProfileClick, size }) => { 25 | let realSize = size; 26 | if (size === null) { 27 | realSize = 28; 28 | } 29 | const $profileRef = useRef(null); 30 | return ( 31 | { 34 | onProfileClick($profileRef, user); 35 | }} 36 | size={realSize} 37 | backgroundURL={user.profileIcon.url ?? null} 38 | bgColor={user.profileIcon.bgColor ?? null} 39 | > 40 | {(!user.profileIcon.url && user.profileIcon.initials) ?? ''} 41 | 42 | ); 43 | }; 44 | 45 | export default ProfileIcon; 46 | -------------------------------------------------------------------------------- /frontend/src/shared/components/ProjectGridItem/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { mixin } from 'shared/utils/styles'; 3 | 4 | export const AddProjectLabel = styled.span` 5 | padding-top: 4px; 6 | font-size: 14px; 7 | color: #c2c6dc; 8 | `; 9 | 10 | export const ProjectContent = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | `; 14 | 15 | export const ProjectTitle = styled.span` 16 | font-size: 18px; 17 | font-weight: 700; 18 | transition: transform 0.25s ease; 19 | `; 20 | export const TeamTitle = styled.span` 21 | margin-top: 5px; 22 | font-size: 14px; 23 | font-weight: normal; 24 | color: #c2c6dc; 25 | `; 26 | 27 | export const ProjectWrapper = styled.div<{ color: string }>` 28 | display: flex; 29 | padding: 15px 25px; border-radius: 20px; 30 | ${mixin.boxShadowCard} 31 | background: ${props => mixin.darken(props.color, 0.35)}; 32 | color: #fff; 33 | cursor: pointer; 34 | margin: 0 10px; 35 | width: 240px; 36 | height: 100px; 37 | transition: transform 0.25s ease; 38 | align-items: center; 39 | justify-content: center; 40 | 41 | &:hover { 42 | transform: translateY(-5px); 43 | } 44 | `; 45 | 46 | export const AddProjectWrapper = styled.div` 47 | display: flex; 48 | padding: 15px 25px; 49 | border-radius: 20px; 50 | ${mixin.boxShadowCard} 51 | border: 1px dashed; 52 | border-color: #c2c6dc; 53 | color: #fff; 54 | cursor: pointer; 55 | margin: 0 10px; 56 | width: 240px; 57 | flex-direction: column; 58 | height: 100px; 59 | transition: transform 0.25s ease; 60 | align-items: center; 61 | justify-content: center; 62 | &:hover { 63 | transform: translateY(-5px); 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /frontend/src/shared/components/ProjectGridItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Plus } from 'shared/icons'; 4 | import { AddProjectWrapper, AddProjectLabel, ProjectWrapper, ProjectContent, ProjectTitle, TeamTitle } from './Styles'; 5 | 6 | type AddProjectItemProps = { 7 | onAddProject: () => void; 8 | }; 9 | export const AddProjectItem: React.FC = ({ onAddProject }) => { 10 | return ( 11 | { 13 | onAddProject(); 14 | }} 15 | > 16 | 17 | New Project 18 | 19 | ); 20 | }; 21 | type Props = { 22 | project: Project; 23 | }; 24 | 25 | const ProjectsList = ({ project }: Props) => { 26 | const color = project.color ?? '#c2c6dc'; 27 | return ( 28 | 29 | 30 | {project.name} 31 | {project.teamTitle} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default ProjectsList; 38 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Sidebar/Styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.div` 4 | position: fixed; 5 | z-index: 99; 6 | top: 0px; 7 | left: 80px; 8 | height: 100vh; 9 | width: 230px; 10 | overflow-x: hidden; 11 | overflow-y: auto; 12 | padding: 0px 16px 24px; 13 | background: rgb(244, 245, 247); 14 | border-right: 1px solid rgb(223, 225, 230); 15 | `; 16 | 17 | export default Container; 18 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Container from './Styles'; 4 | 5 | const Sidebar = () => { 6 | return ; 7 | }; 8 | 9 | export default Sidebar; 10 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Tabs = () => { 5 | return HEllo!; 6 | }; 7 | 8 | export default Tabs; 9 | -------------------------------------------------------------------------------- /frontend/src/shared/components/Textarea/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | import TextareaAutosize from 'react-autosize-textarea'; 3 | 4 | const Textarea = styled(TextareaAutosize)` 5 | border: none; 6 | resize: none; 7 | overflow: hidden; 8 | overflow-wrap: break-word; 9 | background: transparent; 10 | border-radius: 3px; 11 | box-shadow: none; 12 | margin: -4px 0; 13 | 14 | letter-spacing: normal; 15 | word-spacing: normal; 16 | text-transform: none; 17 | text-indent: 0px; 18 | text-shadow: none; 19 | flex-direction: column; 20 | text-align: start; 21 | 22 | color: #c2c6dc; 23 | font-weight: 600; 24 | font-size: 20px; 25 | padding: 3px 10px 3px 8px; 26 | &:focus { 27 | box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px; 28 | } 29 | `; 30 | 31 | export default Textarea; 32 | -------------------------------------------------------------------------------- /frontend/src/shared/constants/keyCodes.ts: -------------------------------------------------------------------------------- 1 | const KeyCodes = { 2 | TAB: 9, 3 | ENTER: 13, 4 | ESCAPE: 27, 5 | SPACE: 32, 6 | ARROW_LEFT: 37, 7 | ARROW_UP: 38, 8 | ARROW_RIGHT: 39, 9 | ARROW_DOWN: 40, 10 | M: 77, 11 | }; 12 | 13 | export default KeyCodes; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/constants/labelColors.ts: -------------------------------------------------------------------------------- 1 | const LabelColors = { 2 | GREEN: '#61bd4f', 3 | YELLOW: '#f2d600', 4 | ORANGE: '#ff9f1a', 5 | RED: '#eb5a46', 6 | PURPLE: '#c377e0', 7 | BLUE: '#0079bf', 8 | SKY: '#00c2e0', 9 | LIME: '#51e898', 10 | PINK: '#ff78cb', 11 | BLACK: '#344563', 12 | }; 13 | 14 | export const DarkLabelColors = { 15 | RED: '#e8384f', 16 | ORANGE: '#fd612c', 17 | YELLOW_ORANGE: '#fd9a00', 18 | YELLOW: '#eec300', 19 | YELLOW_GREEN: '#a4cf30', 20 | GREEN: '#62d26f', 21 | BLUE_GREEN: '#37c5ab', 22 | AQUA: '#20aaea', 23 | BLUE: '#4186e0', 24 | INDIGO: '#7a6ff0', 25 | PURPLE: '#aa62e3', 26 | MAGENTA: '#e362e3', 27 | HOT_PINK: '#ea4e9d', 28 | PINK: '#fc91ad', 29 | COOL_GRAY: '#8da3a6', 30 | }; 31 | 32 | export default LabelColors; 33 | -------------------------------------------------------------------------------- /frontend/src/shared/constants/menuTypes.ts: -------------------------------------------------------------------------------- 1 | const MenuTypes = { 2 | LABEL_MANAGER: 1, 3 | LABEL_EDITOR: 2, 4 | LIST_ACTIONS: 3, 5 | }; 6 | 7 | export default MenuTypes; 8 | -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /frontend/src/shared/fonts/OpenSans-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/frontend/src/shared/fonts/OpenSans-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /frontend/src/shared/graphql/assignTask.graphqls: -------------------------------------------------------------------------------- 1 | mutation assignTask($taskID: UUID!, $userID: UUID!) { 2 | assignTask(input: {taskID: $taskID, userID: $userID}) { 3 | id 4 | assigned { 5 | id 6 | fullName 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/clearAvatarProfile.graphqls: -------------------------------------------------------------------------------- 1 | mutation clearProfileAvatar { 2 | clearProfileAvatar { 3 | id 4 | fullName 5 | profileIcon { 6 | initials 7 | bgColor 8 | url 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/createProject.graphqls: -------------------------------------------------------------------------------- 1 | mutation createProject($teamID: UUID, $name: String!) { 2 | createProject(input: {teamID: $teamID, name: $name}) { 3 | id 4 | shortId 5 | name 6 | team { 7 | id 8 | name 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/createProjectLabel.graphqls: -------------------------------------------------------------------------------- 1 | mutation createProjectLabel($projectID: UUID!, $labelColorID: UUID!, $name: String!) { 2 | createProjectLabel(input:{projectID:$projectID, labelColorID: $labelColorID, name: $name}) { 3 | id 4 | createdDate 5 | labelColor { 6 | id 7 | colorHex 8 | name 9 | position 10 | } 11 | name 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/createTaskGroup.graphqls: -------------------------------------------------------------------------------- 1 | mutation createTaskGroup( $projectID: UUID!, $name: String!, $position: Float! ) { 2 | createTaskGroup( 3 | input: { projectID: $projectID, name: $name, position: $position } 4 | ) { 5 | id 6 | name 7 | position 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/deleteProjectLabel.graphqls: -------------------------------------------------------------------------------- 1 | mutation deleteProjectLabel($projectLabelID: UUID!) { 2 | deleteProjectLabel(input: { projectLabelID:$projectLabelID }) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/deleteTask.graphqls: -------------------------------------------------------------------------------- 1 | mutation deleteTask($taskID: UUID!) { 2 | deleteTask(input: { taskID: $taskID }) { 3 | taskID 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/deleteTaskGroup.graphqls: -------------------------------------------------------------------------------- 1 | mutation deleteTaskGroup($taskGroupID: UUID!) { 2 | deleteTaskGroup(input: { taskGroupID: $taskGroupID }) { 3 | ok 4 | affectedRows 5 | taskGroup { 6 | id 7 | tasks { 8 | id 9 | name 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/findProject.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import TASK_FRAGMENT from './fragments/task'; 3 | 4 | const FIND_PROJECT_QUERY = gql` 5 | query findProject($projectID: String!) { 6 | findProject(input: { projectShortID: $projectID }) { 7 | id 8 | name 9 | publicOn 10 | team { 11 | id 12 | } 13 | members { 14 | id 15 | fullName 16 | username 17 | role { 18 | code 19 | name 20 | } 21 | profileIcon { 22 | url 23 | initials 24 | bgColor 25 | } 26 | } 27 | invitedMembers { 28 | email 29 | invitedOn 30 | } 31 | labels { 32 | id 33 | createdDate 34 | name 35 | labelColor { 36 | id 37 | name 38 | colorHex 39 | position 40 | } 41 | } 42 | taskGroups { 43 | id 44 | name 45 | position 46 | tasks { 47 | ...TaskFields 48 | } 49 | } 50 | } 51 | labelColors { 52 | id 53 | position 54 | colorHex 55 | name 56 | } 57 | users { 58 | id 59 | email 60 | fullName 61 | username 62 | role { 63 | code 64 | name 65 | } 66 | profileIcon { 67 | url 68 | initials 69 | bgColor 70 | } 71 | owned { 72 | teams { 73 | id 74 | name 75 | } 76 | projects { 77 | id 78 | name 79 | } 80 | } 81 | member { 82 | teams { 83 | id 84 | name 85 | } 86 | projects { 87 | id 88 | name 89 | } 90 | } 91 | } 92 | ${TASK_FRAGMENT} 93 | } 94 | `; 95 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/fragments/task.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const TASK_FRAGMENT = gql` 4 | fragment TaskFields on Task { 5 | id 6 | shortId 7 | name 8 | description 9 | dueDate { 10 | at 11 | } 12 | hasTime 13 | complete 14 | watched 15 | completedAt 16 | position 17 | badges { 18 | checklist { 19 | complete 20 | total 21 | } 22 | comments { 23 | unread 24 | total 25 | } 26 | } 27 | taskGroup { 28 | id 29 | name 30 | position 31 | } 32 | labels { 33 | id 34 | assignedDate 35 | projectLabel { 36 | id 37 | name 38 | createdDate 39 | labelColor { 40 | id 41 | colorHex 42 | position 43 | name 44 | } 45 | } 46 | } 47 | assigned { 48 | id 49 | fullName 50 | profileIcon { 51 | url 52 | initials 53 | bgColor 54 | } 55 | } 56 | } 57 | `; 58 | 59 | export default TASK_FRAGMENT; 60 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/getProjects.graphqls: -------------------------------------------------------------------------------- 1 | query getProjects { 2 | organizations { 3 | id 4 | name 5 | } 6 | teams { 7 | id 8 | name 9 | createdAt 10 | } 11 | projects { 12 | id 13 | shortId 14 | name 15 | team { 16 | id 17 | name 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/labels.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import TASK_FRAGMENT from './fragments/task'; 3 | 4 | const FIND_PROJECT_QUERY = gql` 5 | query labels($projectID: UUID!) { 6 | findProject(input: { projectID: $projectID }) { 7 | labels { 8 | id 9 | createdDate 10 | name 11 | labelColor { 12 | id 13 | name 14 | colorHex 15 | position 16 | } 17 | } 18 | } 19 | labelColors { 20 | id 21 | position 22 | colorHex 23 | name 24 | } 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/me.graphqls: -------------------------------------------------------------------------------- 1 | query me { 2 | me { 3 | user { 4 | id 5 | fullName 6 | username 7 | email 8 | bio 9 | profileIcon { 10 | initials 11 | bgColor 12 | url 13 | } 14 | } 15 | teamRoles { 16 | teamID 17 | roleCode 18 | } 19 | projectRoles { 20 | projectID 21 | roleCode 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/myTasks.graphqls: -------------------------------------------------------------------------------- 1 | query myTasks($status: MyTasksStatus!, $sort: MyTasksSort!) { 2 | projects { 3 | id 4 | name 5 | } 6 | myTasks(input: { status: $status, sort: $sort }) { 7 | tasks { 8 | id 9 | shortId 10 | taskGroup { 11 | id 12 | name 13 | } 14 | name 15 | dueDate { 16 | at 17 | } 18 | hasTime 19 | complete 20 | completedAt 21 | } 22 | projects { 23 | projectID 24 | taskID 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/notificationToggleRead.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_MUTATION = gql` 4 | mutation notificationToggleRead($notifiedID: UUID!) { 5 | notificationToggleRead(input: { notifiedID: $notifiedID }) { 6 | id 7 | read 8 | readAt 9 | } 10 | } 11 | `; 12 | 13 | export default CREATE_TASK_MUTATION; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/notifications.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const TOP_NAVBAR_QUERY = gql` 4 | query notifications($limit: Int!, $cursor: String, $filter: NotificationFilter!) { 5 | notified(input: { limit: $limit, cursor: $cursor, filter: $filter }) { 6 | totalCount 7 | pageInfo { 8 | endCursor 9 | hasNextPage 10 | } 11 | notified { 12 | id 13 | read 14 | readAt 15 | notification { 16 | id 17 | actionType 18 | data { 19 | key 20 | value 21 | } 22 | causedBy { 23 | username 24 | fullname 25 | id 26 | } 27 | createdAt 28 | } 29 | } 30 | } 31 | } 32 | `; 33 | 34 | export default TOP_NAVBAR_QUERY; 35 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/notifictionMarkAllRead.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_MUTATION = gql` 4 | mutation notificationMarkAllRead { 5 | notificationMarkAllRead { 6 | success 7 | } 8 | } 9 | `; 10 | 11 | export default CREATE_TASK_MUTATION; 12 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/onNotificationAdded.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import TASK_FRAGMENT from './fragments/task'; 3 | 4 | const FIND_PROJECT_QUERY = gql` 5 | subscription notificationAdded { 6 | notificationAdded { 7 | id 8 | read 9 | readAt 10 | notification { 11 | id 12 | actionType 13 | data { 14 | key 15 | value 16 | } 17 | causedBy { 18 | username 19 | fullname 20 | id 21 | } 22 | createdAt 23 | } 24 | } 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/project/deleteProject.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_PROJECT_MUTATION = gql` 4 | mutation deleteProject($projectID: UUID!) { 5 | deleteProject(input: { projectID: $projectID }) { 6 | ok 7 | project { 8 | id 9 | } 10 | } 11 | } 12 | `; 13 | 14 | export default DELETE_PROJECT_MUTATION; 15 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/project/deleteProjectInvitedMember.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_PROJECT_INVITED_MEMBER_MUTATION = gql` 4 | mutation deleteInvitedProjectMember($projectID: UUID!, $email: String!) { 5 | deleteInvitedProjectMember(input: { projectID: $projectID, email: $email }) { 6 | invitedMember { 7 | email 8 | } 9 | } 10 | } 11 | `; 12 | 13 | export default DELETE_PROJECT_INVITED_MEMBER_MUTATION; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/project/deleteProjectMember.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_PROJECT_MEMBER_MUTATION = gql` 4 | mutation deleteProjectMember($projectID: UUID!, $userID: UUID!) { 5 | deleteProjectMember(input: { projectID: $projectID, userID: $userID }) { 6 | ok 7 | member { 8 | id 9 | } 10 | projectID 11 | } 12 | } 13 | `; 14 | 15 | export default DELETE_PROJECT_MEMBER_MUTATION; 16 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/project/inviteProjectMembers.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const INVITE_PROJECT_MEMBERS_MUTATION = gql` 4 | mutation inviteProjectMembers($projectID: UUID!, $members: [MemberInvite!]!) { 5 | inviteProjectMembers(input: { projectID: $projectID, members: $members }) { 6 | ok 7 | invitedMembers { 8 | email 9 | invitedOn 10 | } 11 | members { 12 | id 13 | fullName 14 | profileIcon { 15 | url 16 | initials 17 | bgColor 18 | } 19 | username 20 | role { 21 | code 22 | name 23 | } 24 | } 25 | } 26 | } 27 | `; 28 | 29 | export default INVITE_PROJECT_MEMBERS_MUTATION; 30 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/project/updateProjectMemberRole.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const UPDATE_PROJECT_MEMBER_ROLE_MUTATION = gql` 4 | mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) { 5 | updateProjectMemberRole(input: { projectID: $projectID, userID: $userID, roleCode: $roleCode }) { 6 | ok 7 | member { 8 | id 9 | role { 10 | code 11 | name 12 | } 13 | } 14 | } 15 | } 16 | `; 17 | 18 | export default UPDATE_PROJECT_MEMBER_ROLE_MUTATION; 19 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/createTask.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import TASK_FRAGMENT from '../fragments/task'; 3 | 4 | const CREATE_TASK_MUTATION = gql` 5 | mutation createTask($taskGroupID: UUID!, $name: String!, $position: Float!, $assigned: [UUID!]) { 6 | createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position, assigned: $assigned }) { 7 | ...TaskFields 8 | } 9 | } 10 | ${TASK_FRAGMENT} 11 | `; 12 | 13 | export default CREATE_TASK_MUTATION; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/createTaskChecklist.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_CHECKLIST_MUTATION = gql` 4 | mutation createTaskChecklist($taskID: UUID!, $name: String!, $position: Float!) { 5 | createTaskChecklist(input: { taskID: $taskID, name: $name, position: $position }) { 6 | id 7 | name 8 | position 9 | items { 10 | id 11 | name 12 | taskChecklistID 13 | complete 14 | position 15 | } 16 | } 17 | } 18 | `; 19 | 20 | export default CREATE_TASK_CHECKLIST_MUTATION; 21 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/createTaskChecklistItem.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_CHECKLIST_ITEM = gql` 4 | mutation createTaskChecklistItem($taskChecklistID: UUID!, $name: String!, $position: Float!) { 5 | createTaskChecklistItem(input: { taskChecklistID: $taskChecklistID, name: $name, position: $position }) { 6 | id 7 | name 8 | taskChecklistID 9 | position 10 | complete 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/createTaskComment.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_MUTATION = gql` 4 | mutation createTaskComment($taskID: UUID!, $message: String!) { 5 | createTaskComment(input: { taskID: $taskID, message: $message }) { 6 | taskID 7 | comment { 8 | id 9 | message 10 | pinned 11 | createdAt 12 | updatedAt 13 | createdBy { 14 | id 15 | fullName 16 | profileIcon { 17 | initials 18 | bgColor 19 | url 20 | } 21 | } 22 | } 23 | } 24 | } 25 | `; 26 | 27 | export default CREATE_TASK_MUTATION; 28 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/deleteTaskChecklist.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const DELETE_TASK_CHECKLIST_MUTATION = gql` 4 | mutation deleteTaskChecklist($taskChecklistID: UUID!) { 5 | deleteTaskChecklist(input: { taskChecklistID: $taskChecklistID }) { 6 | ok 7 | taskChecklist { 8 | id 9 | } 10 | } 11 | } 12 | `; 13 | 14 | export default DELETE_TASK_CHECKLIST_MUTATION; 15 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/deleteTaskChecklistItem.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const DELETE_TASK_CHECKLIST_ITEM = gql` 4 | mutation deleteTaskChecklistItem($taskChecklistItemID: UUID!) { 5 | deleteTaskChecklistItem(input: { taskChecklistItemID: $taskChecklistItemID }) { 6 | ok 7 | taskChecklistItem { 8 | id 9 | taskChecklistID 10 | } 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/deleteTaskComment.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_MUTATION = gql` 4 | mutation deleteTaskComment($commentID: UUID!) { 5 | deleteTaskComment(input: { commentID: $commentID }) { 6 | commentID 7 | } 8 | } 9 | `; 10 | 11 | export default CREATE_TASK_MUTATION; 12 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/setTaskChecklistItemComplete.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const SET_TASK_CHECKLIST_ITEM_COMPLETE = gql` 4 | mutation setTaskChecklistItemComplete($taskChecklistItemID: UUID!, $complete: Boolean!) { 5 | setTaskChecklistItemComplete(input: { taskChecklistItemID: $taskChecklistItemID, complete: $complete }) { 6 | id 7 | complete 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/setTaskComplete.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import TASK_FRAGMENT from '../fragments/task'; 3 | 4 | const UPDATE_TASK_GROUP_NAME_MUTATION = gql` 5 | mutation setTaskComplete($taskID: UUID!, $complete: Boolean!) { 6 | setTaskComplete(input: { taskID: $taskID, complete: $complete }) { 7 | ...TaskFields 8 | } 9 | ${TASK_FRAGMENT} 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/toggleTaskWatcher.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_MUTATION = gql` 4 | mutation toggleTaskWatch($taskID: UUID!) { 5 | toggleTaskWatch(input: { taskID: $taskID }) { 6 | id 7 | watched 8 | } 9 | } 10 | `; 11 | 12 | export default CREATE_TASK_MUTATION; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/updateTaskChecklistItemLocation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const UPDATE_TASK_CHECKLIST_ITEM_LOCATION_MUTATION = gql` 4 | mutation updateTaskChecklistItemLocation($taskChecklistID: UUID!, $taskChecklistItemID: UUID!, $position: Float!) { 5 | updateTaskChecklistItemLocation( 6 | input: { taskChecklistID: $taskChecklistID, taskChecklistItemID: $taskChecklistItemID, position: $position } 7 | ) { 8 | taskChecklistID 9 | prevChecklistID 10 | checklistItem { 11 | id 12 | taskChecklistID 13 | position 14 | } 15 | } 16 | } 17 | `; 18 | 19 | export default UPDATE_TASK_CHECKLIST_ITEM_LOCATION_MUTATION; 20 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/updateTaskChecklistItemName.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const UPDATE_TASK_CHECKLIST_ITEM_NAME = gql` 4 | mutation updateTaskChecklistItemName($taskChecklistItemID: UUID!, $name: String!) { 5 | updateTaskChecklistItemName(input: { taskChecklistItemID: $taskChecklistItemID, name: $name }) { 6 | id 7 | name 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/updateTaskChecklistLocation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const UPDATE_TASK_CHECKLIST_LOCATION_MUTATION = gql` 4 | mutation updateTaskChecklistLocation($taskChecklistID: UUID!, $position: Float!) { 5 | updateTaskChecklistLocation(input: { taskChecklistID: $taskChecklistID, position: $position }) { 6 | checklist { 7 | id 8 | position 9 | } 10 | } 11 | } 12 | `; 13 | 14 | export default UPDATE_TASK_CHECKLIST_LOCATION_MUTATION; 15 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/updateTaskChecklistName.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const UPDATE_TASK_CHECKLIST_NAME_MUTATION = gql` 4 | mutation updateTaskChecklistName($taskChecklistID: UUID!, $name: String!) { 5 | updateTaskChecklistName(input: { taskChecklistID: $taskChecklistID, name: $name }) { 6 | id 7 | name 8 | position 9 | items { 10 | id 11 | name 12 | taskChecklistID 13 | complete 14 | position 15 | } 16 | } 17 | } 18 | `; 19 | export default UPDATE_TASK_CHECKLIST_NAME_MUTATION; 20 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/task/updateTaskComment.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const CREATE_TASK_MUTATION = gql` 4 | mutation updateTaskComment($commentID: UUID!, $message: String!) { 5 | updateTaskComment(input: { commentID: $commentID, message: $message }) { 6 | comment { 7 | id 8 | updatedAt 9 | message 10 | } 11 | } 12 | } 13 | `; 14 | 15 | export default CREATE_TASK_MUTATION; 16 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/taskGroup/deleteTaskGroupTasks.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const DELETE_TASK_GROUP_TASKS_MUTATION = gql` 4 | mutation deleteTaskGroupTasks($taskGroupID: UUID!) { 5 | deleteTaskGroupTasks(input: { taskGroupID: $taskGroupID }) { 6 | tasks 7 | taskGroupID 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/taskGroup/duplicateTaskGroup.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import TASK_FRAGMENT from '../fragments/task'; 3 | 4 | const DUPLICATE_TASK_GROUP_MUTATION = gql` 5 | mutation duplicateTaskGroup($taskGroupID: UUID!, $name: String!, $position: Float!, $projectID: UUID!) { 6 | duplicateTaskGroup( 7 | input: { 8 | projectID: $projectID 9 | taskGroupID: $taskGroupID 10 | name: $name 11 | position: $position 12 | } 13 | ) { 14 | taskGroup { 15 | id 16 | name 17 | position 18 | tasks { 19 | ...TaskFields 20 | } 21 | } 22 | } 23 | 24 | ${TASK_FRAGMENT} 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/taskGroup/sortTaskGroup.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const SORT_TASK_GROUP_MUTATION = gql` 4 | mutation sortTaskGroup($tasks: [TaskPositionUpdate!]!, $taskGroupID: UUID!) { 5 | sortTaskGroup(input: { taskGroupID: $taskGroupID, tasks: $tasks }) { 6 | taskGroupID 7 | tasks { 8 | id 9 | position 10 | } 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/taskGroup/updateTaskGroupName.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import TASK_FRAGMENT from '../fragments/task'; 3 | 4 | const UPDATE_TASK_GROUP_NAME_MUTATION = gql` 5 | mutation updateTaskGroupName($taskGroupID: UUID!, $name: String!) { 6 | updateTaskGroupName(input:{taskGroupID:$taskGroupID, name:$name}) { 7 | id 8 | name 9 | } 10 | ${TASK_FRAGMENT} 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/team/createTeam.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const CREATE_TEAM_MUTATION = gql` 4 | mutation createTeam($name: String!, $organizationID: UUID!) { 5 | createTeam(input: { name: $name, organizationID: $organizationID }) { 6 | id 7 | createdAt 8 | name 9 | } 10 | } 11 | `; 12 | 13 | export default CREATE_TEAM_MUTATION; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/team/createTeamMember.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const CREATE_TEAM_MEMBER_MUTATION = gql` 4 | mutation createTeamMember($userID: UUID!, $teamID: UUID!) { 5 | createTeamMember(input: { userID: $userID, teamID: $teamID }) { 6 | team { 7 | id 8 | } 9 | teamMember { 10 | id 11 | username 12 | fullName 13 | role { 14 | code 15 | name 16 | } 17 | profileIcon { 18 | url 19 | initials 20 | bgColor 21 | } 22 | } 23 | } 24 | } 25 | `; 26 | 27 | export default CREATE_TEAM_MEMBER_MUTATION; 28 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/team/deleteTeam.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_TEAM_MUTATION = gql` 4 | mutation deleteTeam($teamID: UUID!) { 5 | deleteTeam(input: { teamID: $teamID }) { 6 | ok 7 | team { 8 | id 9 | } 10 | } 11 | } 12 | `; 13 | 14 | export default DELETE_TEAM_MUTATION; 15 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/team/deleteTeamMember.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_TEAM_MEMBER_MUTATION = gql` 4 | mutation deleteTeamMember($teamID: UUID!, $userID: UUID!, $newOwnerID: UUID) { 5 | deleteTeamMember(input: { teamID: $teamID, userID: $userID, newOwnerID: $newOwnerID }) { 6 | teamID 7 | userID 8 | } 9 | } 10 | `; 11 | 12 | export default DELETE_TEAM_MEMBER_MUTATION; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/team/updateTeamMemberRole.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const UPDATE_TEAM_MEMBER_ROLE_MUTATION = gql` 4 | mutation updateTeamMemberRole($teamID: UUID!, $userID: UUID!, $roleCode: RoleCode!) { 5 | updateTeamMemberRole(input: { teamID: $teamID, userID: $userID, roleCode: $roleCode }) { 6 | member { 7 | id 8 | role { 9 | code 10 | name 11 | } 12 | } 13 | teamID 14 | } 15 | } 16 | `; 17 | 18 | export default UPDATE_TEAM_MEMBER_ROLE_MUTATION; 19 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/toggleProjectVisibility.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_PROJECT_MUTATION = gql` 4 | mutation toggleProjectVisibility($projectID: UUID!, $isPublic: Boolean!) { 5 | toggleProjectVisibility(input: { projectID: $projectID, isPublic: $isPublic }) { 6 | project { 7 | id 8 | publicOn 9 | } 10 | } 11 | } 12 | `; 13 | 14 | export default DELETE_PROJECT_MUTATION; 15 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/toggleTaskLabel.graphqls: -------------------------------------------------------------------------------- 1 | mutation toggleTaskLabel($taskID: UUID!, $projectLabelID: UUID!) { 2 | toggleTaskLabel( 3 | input: { 4 | taskID: $taskID, 5 | projectLabelID: $projectLabelID 6 | } 7 | ) { 8 | active 9 | task { 10 | id 11 | labels { 12 | id 13 | assignedDate 14 | projectLabel { 15 | id 16 | createdDate 17 | labelColor { 18 | id 19 | colorHex 20 | name 21 | position 22 | } 23 | name 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/topNavbar.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const TOP_NAVBAR_QUERY = gql` 4 | query topNavbar { 5 | notifications { 6 | id 7 | read 8 | readAt 9 | notification { 10 | id 11 | actionType 12 | causedBy { 13 | username 14 | fullname 15 | id 16 | } 17 | createdAt 18 | } 19 | } 20 | me { 21 | user { 22 | id 23 | fullName 24 | profileIcon { 25 | initials 26 | bgColor 27 | url 28 | } 29 | } 30 | teamRoles { 31 | teamID 32 | roleCode 33 | } 34 | projectRoles { 35 | projectID 36 | roleCode 37 | } 38 | } 39 | } 40 | `; 41 | 42 | export default TOP_NAVBAR_QUERY; 43 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/unassignTask.graphqls: -------------------------------------------------------------------------------- 1 | mutation unassignTask($taskID: UUID!, $userID: UUID!) { 2 | unassignTask(input: {taskID: $taskID, userID: $userID}) { 3 | assigned { 4 | id 5 | fullName 6 | } 7 | id 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/unreadNotifications.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const TOP_NAVBAR_QUERY = gql` 4 | query hasUnreadNotifications { 5 | hasUnreadNotifications { 6 | unread 7 | } 8 | } 9 | `; 10 | 11 | export default TOP_NAVBAR_QUERY; 12 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/updateProjectLabel.graphqls: -------------------------------------------------------------------------------- 1 | mutation updateProjectLabel($projectLabelID: UUID!, $labelColorID: UUID!, $name: String!) { 2 | updateProjectLabel(input:{projectLabelID:$projectLabelID, labelColorID: $labelColorID, name: $name}) { 3 | id 4 | createdDate 5 | labelColor { 6 | id 7 | colorHex 8 | name 9 | position 10 | } 11 | name 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/updateProjectName.graphqls: -------------------------------------------------------------------------------- 1 | mutation updateProjectName($projectID: UUID!, $name: String!) { 2 | updateProjectName(input: {projectID: $projectID, name: $name}) { 3 | id 4 | name 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/updateTaskDescription.graphqls: -------------------------------------------------------------------------------- 1 | mutation updateTaskDescription($taskID: UUID!, $description: String!) { 2 | updateTaskDescription(input: {taskID: $taskID, description: $description}) { 3 | id 4 | description 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/updateTaskDueDate.graphqls: -------------------------------------------------------------------------------- 1 | mutation updateTaskDueDate($taskID: UUID!, $dueDate: Time, $hasTime: Boolean!, 2 | $createNotifications: [CreateTaskDueDateNotification!]!, 3 | $updateNotifications: [UpdateTaskDueDateNotification!]! 4 | $deleteNotifications: [DeleteTaskDueDateNotification!]! 5 | ) { 6 | updateTaskDueDate ( 7 | input: { 8 | taskID: $taskID 9 | dueDate: $dueDate 10 | hasTime: $hasTime 11 | } 12 | ) { 13 | id 14 | dueDate { 15 | at 16 | } 17 | hasTime 18 | } 19 | createTaskDueDateNotifications(input: $createNotifications) { 20 | notifications { 21 | id 22 | period 23 | duration 24 | } 25 | } 26 | updateTaskDueDateNotifications(input: $updateNotifications) { 27 | notifications { 28 | id 29 | period 30 | duration 31 | } 32 | } 33 | deleteTaskDueDateNotifications(input: $deleteNotifications) { 34 | notifications 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/updateTaskGroupLocation.graphqls: -------------------------------------------------------------------------------- 1 | mutation updateTaskGroupLocation($taskGroupID: UUID!, $position: Float!) { 2 | updateTaskGroupLocation(input:{taskGroupID:$taskGroupID, position: $position}) { 3 | id 4 | position 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/updateTaskLocation.graphqls: -------------------------------------------------------------------------------- 1 | mutation updateTaskLocation($taskID: UUID!, $taskGroupID: UUID!, $position: Float!) { 2 | updateTaskLocation(input: { taskID: $taskID, taskGroupID: $taskGroupID, position: $position }) { 3 | previousTaskGroupID 4 | task { 5 | id 6 | createdAt 7 | name 8 | position 9 | taskGroup { 10 | id 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/updateTaskName.graphqls: -------------------------------------------------------------------------------- 1 | mutation updateTaskName($taskID: UUID!, $name: String!) { 2 | updateTaskName(input: { taskID: $taskID, name: $name }) { 3 | id 4 | name 5 | position 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/user/createUser.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const CREATE_USER_MUTATION = gql` 4 | mutation createUserAccount( 5 | $username: String! 6 | $roleCode: String! 7 | $email: String! 8 | $fullName: String! 9 | $initials: String! 10 | $password: String! 11 | ) { 12 | createUserAccount( 13 | input: { 14 | roleCode: $roleCode 15 | username: $username 16 | email: $email 17 | fullName: $fullName 18 | initials: $initials 19 | password: $password 20 | } 21 | ) { 22 | id 23 | email 24 | fullName 25 | initials 26 | username 27 | bio 28 | profileIcon { 29 | url 30 | initials 31 | bgColor 32 | } 33 | role { 34 | code 35 | name 36 | } 37 | owned { 38 | teams { 39 | id 40 | name 41 | } 42 | projects { 43 | id 44 | name 45 | } 46 | } 47 | member { 48 | teams { 49 | id 50 | name 51 | } 52 | projects { 53 | id 54 | name 55 | } 56 | } 57 | } 58 | } 59 | `; 60 | 61 | export default CREATE_USER_MUTATION; 62 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/user/deleteInvitedUser.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_INVITED_USER_MUTATION = gql` 4 | mutation deleteInvitedUserAccount($invitedUserID: UUID!) { 5 | deleteInvitedUserAccount(input: { invitedUserID: $invitedUserID }) { 6 | invitedUser { 7 | id 8 | } 9 | } 10 | } 11 | `; 12 | 13 | export default DELETE_INVITED_USER_MUTATION; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/user/deleteUser.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const DELETE_USER_MUTATION = gql` 4 | mutation deleteUserAccount($userID: UUID!, $newOwnerID: UUID) { 5 | deleteUserAccount(input: { userID: $userID, newOwnerID: $newOwnerID }) { 6 | ok 7 | userAccount { 8 | id 9 | } 10 | } 11 | } 12 | `; 13 | 14 | export default DELETE_USER_MUTATION; 15 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/user/updateUserInfo.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const UPDATE_USER_INFO_MUTATION = gql` 4 | mutation updateUserInfo($name: String!, $initials: String!, $email: String!, $bio: String!) { 5 | updateUserInfo(input: { name: $name, initials: $initials, email: $email, bio: $bio }) { 6 | user { 7 | id 8 | email 9 | fullName 10 | bio 11 | profileIcon { 12 | initials 13 | } 14 | } 15 | } 16 | } 17 | `; 18 | 19 | export default UPDATE_USER_INFO_MUTATION; 20 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/user/updateUserPassword.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const UPDATE_USER_PASSWORD_MUTATION = gql` 4 | mutation updateUserPassword($userID: UUID!, $password: String!) { 5 | updateUserPassword(input: { userID: $userID, password: $password }) { 6 | ok 7 | } 8 | } 9 | `; 10 | 11 | export default UPDATE_USER_PASSWORD_MUTATION; 12 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/user/updateUserRole.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const UPDATE_USER_ROLE_MUTATION = gql` 4 | mutation updateUserRole($userID: UUID!, $roleCode: RoleCode!) { 5 | updateUserRole(input: { userID: $userID, roleCode: $roleCode }) { 6 | user { 7 | id 8 | role { 9 | code 10 | name 11 | } 12 | } 13 | } 14 | } 15 | `; 16 | 17 | export default UPDATE_USER_ROLE_MUTATION; 18 | -------------------------------------------------------------------------------- /frontend/src/shared/graphql/users.graphqls: -------------------------------------------------------------------------------- 1 | query users { 2 | invitedUsers { 3 | id 4 | email 5 | invitedOn 6 | } 7 | users { 8 | id 9 | email 10 | fullName 11 | username 12 | role { 13 | code 14 | name 15 | } 16 | profileIcon { 17 | url 18 | initials 19 | bgColor 20 | } 21 | owned { 22 | teams { 23 | id 24 | name 25 | } 26 | projects { 27 | id 28 | name 29 | } 30 | } 31 | member { 32 | teams { 33 | id 34 | name 35 | } 36 | projects { 37 | id 38 | name 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/shared/hooks/memoize.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { isEqual } from 'lodash'; 3 | 4 | const useDeepCompareMemoize = (value: any) => { 5 | const valueRef = useRef(); 6 | 7 | if (!isEqual(value, valueRef.current)) { 8 | valueRef.current = value; 9 | } 10 | return valueRef.current; 11 | }; 12 | 13 | export default useDeepCompareMemoize; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/hooks/onEscapeKeyDown.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import KeyCodes from 'shared/constants/keyCodes'; 3 | 4 | const useOnEscapeKeyDown = (isListening: boolean, onEscapeKeyDown: () => void) => { 5 | useEffect(() => { 6 | const handleKeyDown = (event: any) => { 7 | if (event.keyCode === KeyCodes.ESCAPE) { 8 | onEscapeKeyDown(); 9 | } 10 | }; 11 | if (isListening) { 12 | document.addEventListener('keydown', handleKeyDown); 13 | } 14 | return () => { 15 | document.removeEventListener('keydown', handleKeyDown); 16 | }; 17 | }, [isListening, onEscapeKeyDown]); 18 | }; 19 | export default useOnEscapeKeyDown; 20 | -------------------------------------------------------------------------------- /frontend/src/shared/hooks/useStickyState.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function useStickyState(defaultValue: any, key: string): [T, React.Dispatch>] { 4 | const [value, setValue] = React.useState(() => { 5 | const stickyValue = window.localStorage.getItem(key); 6 | return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue; 7 | }); 8 | React.useEffect(() => { 9 | window.localStorage.setItem(key, JSON.stringify(value)); 10 | }, [key, value]); 11 | return [value, setValue]; 12 | } 13 | 14 | export default useStickyState; 15 | -------------------------------------------------------------------------------- /frontend/src/shared/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from 'react'; 2 | 3 | export default function useWindowSize() { 4 | const [size, setSize] = useState([0, 0]); 5 | useLayoutEffect(() => { 6 | function updateSize() { 7 | setSize([window.innerWidth, window.innerHeight]); 8 | } 9 | window.addEventListener('resize', updateSize); 10 | updateSize(); 11 | return () => window.removeEventListener('resize', updateSize); 12 | }, []); 13 | return size; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/AccountPlus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const AccountPlus: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default AccountPlus; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/AngleDown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | export const AngleDown: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | type Props = { 13 | width?: number | string; 14 | height?: number | string; 15 | color?: string; 16 | }; 17 | 18 | const AngleDownOld = ({ width, height, color }: Props) => { 19 | return ( 20 | 21 | 25 | 26 | ); 27 | }; 28 | 29 | AngleDownOld.defaultProps = { 30 | width: 24, 31 | height: 16, 32 | color: '#000', 33 | }; 34 | 35 | export default AngleDownOld; 36 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/AngleLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | }; 7 | 8 | const AngleLeft = ({ size, color }: Props) => { 9 | return ( 10 | 11 | 15 | 16 | ); 17 | }; 18 | 19 | export default AngleLeft; 20 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/ArrowDown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const ArrowDown: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ArrowDown; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/ArrowLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | width: number | string; 5 | height: number | string; 6 | color: string; 7 | }; 8 | 9 | const ArrowLeft = ({ width, height, color }: Props) => { 10 | return ( 11 | 12 | 16 | 17 | ); 18 | }; 19 | 20 | export default ArrowLeft; 21 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/At.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const At: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default At; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/BarChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const BarChart: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default BarChart; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Bell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Bell: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Bell; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Bin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | }; 7 | 8 | const Bin = ({ size, color }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Bin; 18 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Bolt.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Bolt: React.FC = ({ width = '16px', height = '16px' }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Bolt; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Briefcase.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Briefcase: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Briefcase; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Bubble.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Bubble: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Bubble; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Calender: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Calender; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/CaretDown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const CaretDown: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CaretDown; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/CaretRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const CaretRight: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CaretRight; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/CheckCircle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const CheckCircle: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CheckCircle; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/CheckCircleOutline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const CheckCircleOutline: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CheckCircleOutline; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/CheckSquare.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const CheckSquare: React.FC = ({ width = '16px', height = '16px', onClick, className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CheckSquare; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/CheckSquareOutline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const CheckSquareOutline: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CheckSquareOutline; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Checkmark.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Checkmark: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Checkmark; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/ChevronRight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const ChevronRight: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ChevronRight; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Circle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Circle: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Circle; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/CircleSolid.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const CircleSolid: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CircleSolid; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Clock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Clock: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Clock; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Clone.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Clone: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Clone; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Cog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | }; 7 | 8 | const Cog = ({ size, color }: Props) => { 9 | return ( 10 | 11 | 15 | 16 | ); 17 | }; 18 | 19 | export default Cog; 20 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Cross.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Cross: React.FC = ({ width = '16px', height = '16px', className, onClick }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Cross; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Crown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Crown: React.FC = ({ width = '16px', height = '16px', onClick, className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Crown; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Dot.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Dot: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Dot; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/DotCircle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const DotCircle: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default DotCircle; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/DoubleChevronUp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const DoubleChevronUp: React.FC = ({ width = '16px', height = '16px', onClick, className }) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default DoubleChevronUp; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Ellipsis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | vertical: boolean; 7 | }; 8 | 9 | const Ellipsis = ({ size, color, vertical }: Props) => { 10 | if (vertical) { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | return ( 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Ellipsis; 25 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Exit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | }; 7 | 8 | const Exit = ({ size, color }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Exit; 17 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Eye.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Eye: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Eye; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/EyeSlash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const EyeSlash: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default EyeSlash; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Filter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Filter: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Filter; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Checkmark: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Checkmark; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/macro'; 3 | 4 | export type IconProps = { 5 | width: number | string; 6 | height: number | string; 7 | className?: string; 8 | onClick?: () => void; 9 | }; 10 | 11 | type Props = { 12 | width: number | string; 13 | height: number | string; 14 | viewBox: string; 15 | className?: string; 16 | onClick?: () => void; 17 | }; 18 | 19 | const Svg = styled.svg` 20 | fill: ${props => props.theme.colors.text.primary}; 21 | stroke: ${props => props.theme.colors.text.primary}; 22 | `; 23 | 24 | const Icon: React.FC = ({ width, height, viewBox, className, onClick, children }) => { 25 | return ( 26 | { 28 | if (onClick) { 29 | onClick(); 30 | } 31 | }} 32 | className={className} 33 | width={width} 34 | height={height} 35 | xmlns="http://www.w3.org/2000/svg" 36 | viewBox={viewBox} 37 | > 38 | {children} 39 | 40 | ); 41 | }; 42 | 43 | export default Icon; 44 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const List: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default List; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/ListUnordered.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const ListUnordered: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ListUnordered; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Lock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Lock: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Lock; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Paperclip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Paperclip: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Paperclip; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Pencil.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Sort: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Sort; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Plus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Plus: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Plus; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Question.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | }; 7 | 8 | const Question = ({ size, color }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Question; 17 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Share.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Share: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Share; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Smile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Smile: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Smile; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Sort.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Sort: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Sort; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Square.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Square: React.FC = ({ width = '16px', height = '16px', className, onClick }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Square; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Stack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | }; 7 | 8 | const Stack = ({ size, color }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Stack; 17 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Star.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | width: number | string; 5 | height: number | string; 6 | color: string; 7 | filled: boolean; 8 | }; 9 | 10 | const Star = ({ width, height, color, filled }: Props) => { 11 | return ( 12 | 13 | {filled ? ( 14 | 18 | ) : ( 19 | 23 | )} 24 | 25 | ); 26 | }; 27 | 28 | export default Star; 29 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Tags.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Tags: React.FC = ({ width = '16px', height = '16px' }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Tags; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Task.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Task: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Task; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/ToggleOn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Icon, { IconProps } from './Icon'; 4 | 5 | const ToggleOn: React.FC = ({ width = '16px', height = '16px' }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default ToggleOn; 14 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Trash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const Trash: React.FC = ({ width = '16px', height = '16px', className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Trash; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/User.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const User: React.FC = ({ width = '16px', height = '16px', className, onClick }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default User; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/UserCircle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const UserCircle: React.FC = ({ width = '16px', height = '16px', className, onClick }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default UserCircle; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/UserPlus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon, { IconProps } from './Icon'; 3 | 4 | const UserPlus: React.FC = ({ width = '16px', height = '16px', onClick, className }) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default UserPlus; 13 | -------------------------------------------------------------------------------- /frontend/src/shared/icons/Users.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | size: number | string; 5 | color: string; 6 | }; 7 | 8 | const Users = ({ size, color }: Props) => { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Users; 18 | -------------------------------------------------------------------------------- /frontend/src/shared/utils/boundingRect.ts: -------------------------------------------------------------------------------- 1 | export const convertDivElementRefToBounds = ($ref: React.RefObject) => { 2 | if ($ref && $ref.current) { 3 | const bounds = $ref.current.getBoundingClientRect(); 4 | return { 5 | size: { 6 | width: bounds.width, 7 | height: bounds.height, 8 | }, 9 | position: { 10 | left: bounds.left, 11 | right: bounds.right, 12 | top: bounds.top, 13 | bottom: bounds.bottom, 14 | }, 15 | }; 16 | } 17 | return null; 18 | }; 19 | 20 | export default convertDivElementRefToBounds; 21 | -------------------------------------------------------------------------------- /frontend/src/shared/utils/cache.ts: -------------------------------------------------------------------------------- 1 | import { DataProxy } from '@apollo/client'; 2 | import { DocumentNode } from 'graphql'; 3 | 4 | type UpdateCacheFn = (cache: T) => T; 5 | 6 | export function updateApolloCache( 7 | client: DataProxy, 8 | document: DocumentNode, 9 | update: UpdateCacheFn, 10 | variables?: any, 11 | ) { 12 | let queryArgs: DataProxy.Query; 13 | if (variables) { 14 | queryArgs = { 15 | query: document, 16 | variables, 17 | }; 18 | } else { 19 | queryArgs = { 20 | query: document, 21 | }; 22 | } 23 | const cache: T | null = client.readQuery(queryArgs); 24 | if (cache) { 25 | const newCache = update(cache); 26 | client.writeQuery({ 27 | ...queryArgs, 28 | data: newCache, 29 | }); 30 | } 31 | } 32 | 33 | export default updateApolloCache; 34 | -------------------------------------------------------------------------------- /frontend/src/shared/utils/email.ts: -------------------------------------------------------------------------------- 1 | const RFC2822_EMAIL = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; 2 | 3 | export default function isValidEmail(target: string) { 4 | return RFC2822_EMAIL.test(target); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/shared/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | const localStorage = { 2 | NOTIFICATIONS_FILTER: 'notifications_filter', 3 | CARD_LABEL_VARIANT_STORAGE_KEY: 'card_label_variant', 4 | }; 5 | 6 | export default localStorage; 7 | -------------------------------------------------------------------------------- /frontend/src/shared/utils/noop.ts: -------------------------------------------------------------------------------- 1 | export default function NOOP() {} // eslint-disable-line @typescript-eslint/no-empty-function 2 | -------------------------------------------------------------------------------- /frontend/src/shared/utils/polling.ts: -------------------------------------------------------------------------------- 1 | function resolve(interval: number) { 2 | if (process.env.REACT_APP_ENABLE_POLLING === 'true') return interval; 3 | return 0; 4 | } 5 | const polling = { 6 | PROJECTS: resolve(3000), 7 | PROJECT: resolve(3000), 8 | MEMBERS: resolve(3000), 9 | TEAM_PROJECTS: resolve(3000), 10 | TASK_DETAILS: resolve(3000), 11 | UNREAD_NOTIFICATIONS: resolve(30000), 12 | }; 13 | 14 | export default polling; 15 | -------------------------------------------------------------------------------- /frontend/src/styled.d.ts: -------------------------------------------------------------------------------- 1 | // import original module declarations 2 | import 'styled-components'; 3 | 4 | // and extend them! 5 | declare module 'styled-components' { 6 | export interface DefaultTheme { 7 | borderRadius: { 8 | primary: string; 9 | alternate: string; 10 | }; 11 | colors: { 12 | [key: string]: any; 13 | multiColors: string[]; 14 | primary: string; 15 | secondary: string; 16 | success: string; 17 | danger: string; 18 | warning: string; 19 | dark: string; 20 | alternate: string; 21 | text: { 22 | primary: string; 23 | secondary: string; 24 | }; 25 | bg: { 26 | primary: string; 27 | secondary: string; 28 | }; 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "downlevelIteration": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src", 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "types": [ 29 | "react-beautiful-dnd" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jordanknott/taskcafe 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.13.0 7 | github.com/RichardKnop/machinery v1.9.1 8 | github.com/brianvoe/gofakeit/v5 v5.11.2 9 | github.com/go-chi/chi v3.3.2+incompatible 10 | github.com/go-chi/cors v1.2.0 11 | github.com/go-redis/redis v6.15.8+incompatible 12 | github.com/go-redis/redis/v8 v8.0.0-beta.6 13 | github.com/golang-migrate/migrate/v4 v4.11.0 14 | github.com/google/uuid v1.1.1 15 | github.com/jinzhu/now v1.1.1 16 | github.com/jmoiron/sqlx v1.2.0 17 | github.com/lib/pq v1.3.0 18 | github.com/lithammer/fuzzysearch v1.1.0 19 | github.com/magefile/mage v1.11.0 20 | github.com/manifoldco/promptui v0.8.0 21 | github.com/matcornic/hermes/v2 v2.1.0 22 | github.com/pelletier/go-toml v1.8.0 // indirect 23 | github.com/pkg/errors v0.9.1 24 | github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 25 | github.com/sirupsen/logrus v1.4.2 26 | github.com/spf13/cobra v1.0.0 27 | github.com/spf13/pflag v1.0.5 // indirect 28 | github.com/spf13/viper v1.4.0 29 | github.com/vektah/gqlparser/v2 v2.1.0 30 | golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 31 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 32 | gopkg.in/mail.v2 v2.3.1 33 | ) 34 | -------------------------------------------------------------------------------- /gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - internal/graph/schema/*.gql 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: internal/graph/generated.go 8 | package: graph 9 | 10 | # Uncomment to enable federation 11 | # federation: 12 | # filename: graph/generated/federation.go 13 | # package: generated 14 | 15 | # Where should any generated models go? 16 | model: 17 | filename: internal/graph/models_gen.go 18 | package: graph 19 | 20 | # Where should the resolver implementations go? 21 | resolver: 22 | layout: follow-schema 23 | dir: internal/graph 24 | package: graph 25 | filename_template: "{name}.resolvers.go" 26 | 27 | 28 | # Optional: turn on to use []Thing instead of []*Thing 29 | omit_slice_element_pointers: true 30 | 31 | # gqlgen will search for any type names in the schema in these go packages 32 | # if they match it will use them, otherwise it will generate them. 33 | autobind: 34 | - "github.com/jordanknott/taskcafe/internal/db" 35 | 36 | # This section declares type mapping between the GraphQL and go type systems 37 | # 38 | # The first line in each type will be used as defaults for resolver arguments and 39 | # modelgen, the others will be allowed when binding to fields. Configure them to 40 | # your liking 41 | models: 42 | ID: 43 | model: github.com/jordanknott/taskcafe/internal/graph.UUID 44 | Int: 45 | model: 46 | - github.com/99designs/gqlgen/graphql.Int 47 | 48 | UUID: 49 | model: github.com/jordanknott/taskcafe/internal/graph.UUID 50 | -------------------------------------------------------------------------------- /internal/commands/commands_prod.go: -------------------------------------------------------------------------------- 1 | // +build prod 2 | 3 | package commands 4 | 5 | import ( 6 | "github.com/jordanknott/taskcafe/internal/migrations" 7 | ) 8 | 9 | func init() { 10 | migration = migrations.Migrations 11 | } 12 | -------------------------------------------------------------------------------- /internal/db/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | 3 | package db 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | ) 9 | 10 | type DBTX interface { 11 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 12 | PrepareContext(context.Context, string) (*sql.Stmt, error) 13 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 14 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 15 | } 16 | 17 | func New(db DBTX) *Queries { 18 | return &Queries{db: db} 19 | } 20 | 21 | type Queries struct { 22 | db DBTX 23 | } 24 | 25 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 26 | return &Queries{ 27 | db: tx, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/db/organization.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // source: organization.sql 3 | 4 | package db 5 | 6 | import ( 7 | "context" 8 | "time" 9 | ) 10 | 11 | const createOrganization = `-- name: CreateOrganization :one 12 | INSERT INTO organization (created_at, name) VALUES ($1, $2) RETURNING organization_id, created_at, name 13 | ` 14 | 15 | type CreateOrganizationParams struct { 16 | CreatedAt time.Time `json:"created_at"` 17 | Name string `json:"name"` 18 | } 19 | 20 | func (q *Queries) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) { 21 | row := q.db.QueryRowContext(ctx, createOrganization, arg.CreatedAt, arg.Name) 22 | var i Organization 23 | err := row.Scan(&i.OrganizationID, &i.CreatedAt, &i.Name) 24 | return i, err 25 | } 26 | 27 | const getAllOrganizations = `-- name: GetAllOrganizations :many 28 | SELECT organization_id, created_at, name FROM organization 29 | ` 30 | 31 | func (q *Queries) GetAllOrganizations(ctx context.Context) ([]Organization, error) { 32 | rows, err := q.db.QueryContext(ctx, getAllOrganizations) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer rows.Close() 37 | var items []Organization 38 | for rows.Next() { 39 | var i Organization 40 | if err := rows.Scan(&i.OrganizationID, &i.CreatedAt, &i.Name); err != nil { 41 | return nil, err 42 | } 43 | items = append(items, i) 44 | } 45 | if err := rows.Close(); err != nil { 46 | return nil, err 47 | } 48 | if err := rows.Err(); err != nil { 49 | return nil, err 50 | } 51 | return items, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/db/query/label_color.sql: -------------------------------------------------------------------------------- 1 | -- name: GetLabelColorByID :one 2 | SELECT * FROM label_color WHERE label_color_id = $1; 3 | 4 | -- name: GetLabelColors :many 5 | SELECT * FROM label_color; 6 | 7 | -- name: CreateLabelColor :one 8 | INSERT INTO label_color (name, color_hex, position) VALUES ($1, $2, $3) 9 | RETURNING *; 10 | -------------------------------------------------------------------------------- /internal/db/query/organization.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAllOrganizations :many 2 | SELECT * FROM organization; 3 | 4 | -- name: CreateOrganization :one 5 | INSERT INTO organization (created_at, name) VALUES ($1, $2) RETURNING *; 6 | -------------------------------------------------------------------------------- /internal/db/query/project_label.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateProjectLabel :one 2 | INSERT INTO project_label (project_id, label_color_id, created_date, name) 3 | VALUES ($1, $2, $3, $4) RETURNING *; 4 | 5 | -- name: GetProjectLabelsForProject :many 6 | SELECT * FROM project_label WHERE project_id = $1; 7 | 8 | -- name: GetProjectLabelByID :one 9 | SELECT * FROM project_label WHERE project_label_id = $1; 10 | 11 | -- name: DeleteProjectLabelByID :exec 12 | DELETE FROM project_label WHERE project_label_id = $1; 13 | 14 | -- name: UpdateProjectLabelName :one 15 | UPDATE project_label SET name = $2 WHERE project_label_id = $1 RETURNING *; 16 | 17 | -- name: UpdateProjectLabelColor :one 18 | UPDATE project_label SET label_color_id = $2 WHERE project_label_id = $1 RETURNING *; 19 | 20 | -- name: UpdateProjectLabel :one 21 | UPDATE project_label SET name = $2, label_color_id = $3 WHERE project_label_id = $1 RETURNING *; 22 | -------------------------------------------------------------------------------- /internal/db/query/system_options.sql: -------------------------------------------------------------------------------- 1 | -- name: GetSystemOptionByKey :one 2 | SELECT key, value FROM system_options WHERE key = $1; 3 | 4 | -- name: CreateSystemOption :one 5 | INSERT INTO system_options (key, value) VALUES ($1, $2) RETURNING *; 6 | -------------------------------------------------------------------------------- /internal/db/query/task_activity.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateTaskActivity :one 2 | INSERT INTO task_activity (task_id, caused_by, created_at, activity_type_id, data) 3 | VALUES ($1, $2, $3, $4, $5) RETURNING *; 4 | 5 | -- name: GetActivityForTaskID :many 6 | SELECT * FROM task_activity WHERE task_id = $1 AND active = true; 7 | 8 | -- name: GetTemplateForActivityID :one 9 | SELECT template FROM task_activity_type WHERE task_activity_type_id = $1; 10 | 11 | -- name: GetLastMoveForTaskID :one 12 | SELECT active, created_at, data->>'CurTaskGroupID' AS cur_task_group_id, data->>'PrevTaskGroupID' AS prev_task_group_id FROM task_activity 13 | WHERE task_id = $1 AND activity_type_id = 2 AND created_at >= NOW() - INTERVAL '5 minutes' 14 | ORDER BY created_at DESC LIMIT 1; 15 | 16 | -- name: SetInactiveLastMoveForTaskID :exec 17 | UPDATE task_activity SET active = false WHERE task_activity_id = ( 18 | SELECT task_activity_id FROM task_activity AS ta 19 | WHERE ta.activity_type_id = 2 AND ta.task_id = $1 20 | AND ta.created_at >= NOW() - INTERVAL '5 minutes' 21 | ORDER BY created_at DESC LIMIT 1 22 | ); 23 | -------------------------------------------------------------------------------- /internal/db/query/task_assigned.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateTaskAssigned :one 2 | INSERT INTO task_assigned (task_id, user_id, assigned_date) 3 | VALUES($1, $2, $3) RETURNING *; 4 | 5 | -- name: GetAssignedMembersForTask :many 6 | SELECT * FROM task_assigned WHERE task_id = $1; 7 | 8 | -- name: DeleteTaskAssignedByID :one 9 | DELETE FROM task_assigned WHERE task_id = $1 AND user_id = $2 RETURNING *; 10 | -------------------------------------------------------------------------------- /internal/db/query/task_group.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateTaskGroup :one 2 | INSERT INTO task_group (project_id, created_at, name, position) 3 | VALUES($1, $2, $3, $4) RETURNING *; 4 | 5 | -- name: GetTaskGroupsForProject :many 6 | SELECT * FROM task_group WHERE project_id = $1; 7 | 8 | -- name: GetProjectIDForTaskGroup :one 9 | SELECT project_id from task_group WHERE task_group_id = $1; 10 | 11 | -- name: GetAllTaskGroups :many 12 | SELECT * FROM task_group; 13 | 14 | -- name: GetTaskGroupByID :one 15 | SELECT * FROM task_group WHERE task_group_id = $1; 16 | 17 | -- name: SetTaskGroupName :one 18 | UPDATE task_group SET name = $2 WHERE task_group_id = $1 RETURNING *; 19 | 20 | -- name: DeleteTaskGroupByID :execrows 21 | DELETE FROM task_group WHERE task_group_id = $1; 22 | 23 | -- name: UpdateTaskGroupLocation :one 24 | UPDATE task_group SET position = $2 WHERE task_group_id = $1 RETURNING *; 25 | -------------------------------------------------------------------------------- /internal/db/query/task_label.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateTaskLabelForTask :one 2 | INSERT INTO task_label (task_id, project_label_id, assigned_date) 3 | VALUES ($1, $2, $3) RETURNING *; 4 | 5 | -- name: GetTaskLabelsForTaskID :many 6 | SELECT * FROM task_label WHERE task_id = $1; 7 | 8 | -- name: GetTaskLabelByID :one 9 | SELECT * FROM task_label WHERE task_label_id = $1; 10 | 11 | -- name: DeleteTaskLabelByID :exec 12 | DELETE FROM task_label WHERE task_label_id = $1; 13 | 14 | -- name: GetTaskLabelForTaskByProjectLabelID :one 15 | SELECT * FROM task_label WHERE task_id = $1 AND project_label_id = $2; 16 | 17 | -- name: DeleteTaskLabelForTaskByProjectLabelID :exec 18 | DELETE FROM task_label WHERE project_label_id = $2 AND task_id = $1; 19 | -------------------------------------------------------------------------------- /internal/db/query/team.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAllTeams :many 2 | SELECT * FROM team; 3 | 4 | -- name: GetTeamByID :one 5 | SELECT * FROM team WHERE team_id = $1; 6 | 7 | -- name: CreateTeam :one 8 | INSERT INTO team (organization_id, created_at, name) VALUES ($1, $2, $3) RETURNING *; 9 | 10 | -- name: DeleteTeamByID :exec 11 | DELETE FROM team WHERE team_id = $1; 12 | 13 | -- name: GetTeamsForOrganization :many 14 | SELECT * FROM team WHERE organization_id = $1; 15 | 16 | -- name: GetMemberTeamIDsForUserID :many 17 | SELECT team_id FROM team_member WHERE user_id = $1; 18 | 19 | -- name: GetTeamRoleForUserID :one 20 | SELECT team_id, role_code FROM team_member WHERE user_id = $1 AND team_id = $2; 21 | 22 | -- name: GetTeamRolesForUserID :many 23 | SELECT team_id, role_code FROM team_member WHERE user_id = $1; 24 | 25 | -- name: GetTeamsForUserIDWhereAdmin :many 26 | SELECT team.* FROM team_member INNER JOIN team 27 | ON team.team_id = team_member.team_id WHERE (role_code = 'admin' OR role_code = 'member') AND user_id = $1; 28 | -------------------------------------------------------------------------------- /internal/db/query/team_member.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateTeamMember :one 2 | INSERT INTO team_member (team_id, user_id, addedDate, role_code) VALUES ($1, $2, $3, $4) 3 | RETURNING *; 4 | 5 | -- name: GetTeamMembersForTeamID :many 6 | SELECT * FROM team_member WHERE team_id = $1; 7 | 8 | -- name: DeleteTeamMember :exec 9 | DELETE FROM team_member WHERE user_id = $1 AND team_id = $2; 10 | 11 | -- name: GetRoleForTeamMember :one 12 | SELECT code, role.name FROM team_member 13 | INNER JOIN role ON role.code = team_member.role_code 14 | WHERE user_id = $1 AND team_id = $2; 15 | 16 | -- name: UpdateTeamMemberRole :one 17 | UPDATE team_member SET role_code = $3 WHERE user_id = $2 AND team_id = $1 18 | RETURNING *; 19 | 20 | -- name: GetTeamMemberByID :one 21 | SELECT * FROM team_member WHERE team_id = $1 AND user_id = $2; 22 | -------------------------------------------------------------------------------- /internal/db/query/token.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAuthTokenByID :one 2 | SELECT * FROM auth_token WHERE token_id = $1; 3 | 4 | -- name: CreateAuthToken :one 5 | INSERT INTO auth_token (user_id, created_at, expires_at) VALUES ($1, $2, $3) RETURNING *; 6 | 7 | -- name: DeleteAuthTokenByID :exec 8 | DELETE FROM auth_token WHERE token_id = $1; 9 | 10 | -- name: DeleteAuthTokenByUserID :exec 11 | DELETE FROM auth_token WHERE user_id = $1; 12 | 13 | -- name: DeleteExpiredTokens :exec 14 | DELETE FROM auth_token WHERE expires_at <= NOW(); 15 | -------------------------------------------------------------------------------- /internal/db/repository.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | ) 6 | 7 | // Repository contains methods for interacting with a database storage 8 | type Repository struct { 9 | *Queries 10 | db *sqlx.DB 11 | } 12 | 13 | // NewRepository returns an implementation of the Repository interface. 14 | func NewRepository(db *sqlx.DB) *Repository { 15 | return &Repository{ 16 | Queries: New(db.DB), 17 | db: db, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/db/system_options.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // source: system_options.sql 3 | 4 | package db 5 | 6 | import ( 7 | "context" 8 | "database/sql" 9 | ) 10 | 11 | const createSystemOption = `-- name: CreateSystemOption :one 12 | INSERT INTO system_options (key, value) VALUES ($1, $2) RETURNING option_id, key, value 13 | ` 14 | 15 | type CreateSystemOptionParams struct { 16 | Key string `json:"key"` 17 | Value sql.NullString `json:"value"` 18 | } 19 | 20 | func (q *Queries) CreateSystemOption(ctx context.Context, arg CreateSystemOptionParams) (SystemOption, error) { 21 | row := q.db.QueryRowContext(ctx, createSystemOption, arg.Key, arg.Value) 22 | var i SystemOption 23 | err := row.Scan(&i.OptionID, &i.Key, &i.Value) 24 | return i, err 25 | } 26 | 27 | const getSystemOptionByKey = `-- name: GetSystemOptionByKey :one 28 | SELECT key, value FROM system_options WHERE key = $1 29 | ` 30 | 31 | type GetSystemOptionByKeyRow struct { 32 | Key string `json:"key"` 33 | Value sql.NullString `json:"value"` 34 | } 35 | 36 | func (q *Queries) GetSystemOptionByKey(ctx context.Context, key string) (GetSystemOptionByKeyRow, error) { 37 | row := q.db.QueryRowContext(ctx, getSystemOptionByKey, key) 38 | var i GetSystemOptionByKeyRow 39 | err := row.Scan(&i.Key, &i.Value) 40 | return i, err 41 | } 42 | -------------------------------------------------------------------------------- /internal/graph/scalars.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "io" 5 | 6 | "strconv" 7 | 8 | "github.com/99designs/gqlgen/graphql" 9 | "github.com/google/uuid" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // MarshalUUID converts a UUID to JSON string 14 | func MarshalUUID(uuid uuid.UUID) graphql.Marshaler { 15 | return graphql.WriterFunc(func(w io.Writer) { 16 | w.Write([]byte(strconv.Quote(uuid.String()))) 17 | }) 18 | } 19 | 20 | // UnmarshalUUID converts a String to a UUID 21 | func UnmarshalUUID(v interface{}) (uuid.UUID, error) { 22 | if uuidRaw, ok := v.(string); ok { 23 | return uuid.Parse(uuidRaw) 24 | } 25 | return uuid.UUID{}, errors.New("uuid must be a string") 26 | } 27 | -------------------------------------------------------------------------------- /internal/graph/schema/project/_model.gql: -------------------------------------------------------------------------------- 1 | type ProjectPermission { 2 | team: RoleCode! 3 | project: RoleCode! 4 | org: RoleCode! 5 | } 6 | 7 | type Project { 8 | id: ID! 9 | shortId: String! 10 | createdAt: Time! 11 | name: String! 12 | team: Team 13 | taskGroups: [TaskGroup!]! 14 | members: [Member!]! 15 | invitedMembers: [InvitedMember!]! 16 | publicOn: Time 17 | permission: ProjectPermission! 18 | labels: [ProjectLabel!]! 19 | } 20 | 21 | type ProjectLabel { 22 | id: ID! 23 | createdDate: Time! 24 | labelColor: LabelColor! 25 | name: String 26 | } 27 | 28 | type LabelColor { 29 | id: ID! 30 | name: String! 31 | position: Float! 32 | colorHex: String! 33 | } 34 | 35 | type Member { 36 | id: ID! 37 | role: Role! 38 | fullName: String! 39 | username: String! 40 | profileIcon: ProfileIcon! 41 | owned: OwnedList! 42 | member: MemberList! 43 | } 44 | 45 | type InvitedMember { 46 | email: String! 47 | invitedOn: Time! 48 | } 49 | -------------------------------------------------------------------------------- /internal/graph/schema/project/label.gql: -------------------------------------------------------------------------------- 1 | extend type Mutation { 2 | createProjectLabel(input: NewProjectLabel!): 3 | ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT) 4 | deleteProjectLabel(input: DeleteProjectLabel!): 5 | ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT) 6 | updateProjectLabel(input: UpdateProjectLabel!): 7 | ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT) 8 | updateProjectLabelName(input: UpdateProjectLabelName!): 9 | ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT) 10 | updateProjectLabelColor(input: UpdateProjectLabelColor!): 11 | ProjectLabel! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: PROJECT) 12 | } 13 | 14 | input NewProjectLabel { 15 | projectID: UUID! 16 | labelColorID: UUID! 17 | name: String 18 | } 19 | 20 | input DeleteProjectLabel { 21 | projectLabelID: UUID! 22 | } 23 | 24 | input UpdateProjectLabelName { 25 | projectLabelID: UUID! 26 | name: String! 27 | } 28 | 29 | input UpdateProjectLabel { 30 | projectLabelID: UUID! 31 | labelColorID: UUID! 32 | name: String! 33 | } 34 | 35 | input UpdateProjectLabelColor { 36 | projectLabelID: UUID! 37 | labelColorID: UUID! 38 | } 39 | -------------------------------------------------------------------------------- /internal/graph/schema/project/member.gql: -------------------------------------------------------------------------------- 1 | extend type Mutation { 2 | inviteProjectMembers(input: InviteProjectMembers!): 3 | InviteProjectMembersPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) 4 | deleteProjectMember(input: DeleteProjectMember!): 5 | DeleteProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) 6 | updateProjectMemberRole(input: UpdateProjectMemberRole!): 7 | UpdateProjectMemberRolePayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) 8 | 9 | deleteInvitedProjectMember(input: DeleteInvitedProjectMember!): 10 | DeleteInvitedProjectMemberPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) 11 | } 12 | 13 | input DeleteInvitedProjectMember { 14 | projectID: UUID! 15 | email: String! 16 | } 17 | 18 | type DeleteInvitedProjectMemberPayload { 19 | invitedMember: InvitedMember! 20 | } 21 | 22 | input MemberInvite { 23 | userID: UUID 24 | email: String 25 | } 26 | 27 | input InviteProjectMembers { 28 | projectID: UUID! 29 | members: [MemberInvite!]! 30 | } 31 | 32 | type InviteProjectMembersPayload { 33 | ok: Boolean! 34 | projectID: UUID! 35 | members: [Member!]! 36 | invitedMembers: [InvitedMember!]! 37 | } 38 | 39 | input DeleteProjectMember { 40 | projectID: UUID! 41 | userID: UUID! 42 | } 43 | 44 | type DeleteProjectMemberPayload { 45 | ok: Boolean! 46 | member: Member! 47 | projectID: UUID! 48 | } 49 | 50 | input UpdateProjectMemberRole { 51 | projectID: UUID! 52 | userID: UUID! 53 | roleCode: RoleCode! 54 | } 55 | 56 | type UpdateProjectMemberRolePayload { 57 | ok: Boolean! 58 | member: Member! 59 | } 60 | -------------------------------------------------------------------------------- /internal/graph/schema/project/project.gql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | findProject(input: FindProject!): Project! 3 | } 4 | 5 | input FindProject { 6 | projectID: UUID 7 | projectShortID: String 8 | } 9 | 10 | extend type Mutation { 11 | createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) 12 | deleteProject(input: DeleteProject!): 13 | DeleteProjectPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) 14 | updateProjectName(input: UpdateProjectName): 15 | Project! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) 16 | toggleProjectVisibility(input: ToggleProjectVisibility!): ToggleProjectVisibilityPayload! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT) 17 | } 18 | 19 | input ToggleProjectVisibility { 20 | projectID: UUID! 21 | isPublic: Boolean! 22 | } 23 | 24 | type ToggleProjectVisibilityPayload { 25 | project: Project! 26 | } 27 | 28 | input NewProject { 29 | teamID: UUID 30 | name: String! 31 | } 32 | 33 | input UpdateProjectName { 34 | projectID: UUID! 35 | name: String! 36 | } 37 | 38 | input DeleteProject { 39 | projectID: UUID! 40 | } 41 | 42 | type DeleteProjectPayload { 43 | ok: Boolean! 44 | project: Project! 45 | } 46 | -------------------------------------------------------------------------------- /internal/graph/schema/task/comment.gql: -------------------------------------------------------------------------------- 1 | extend type Mutation { 2 | createTaskComment(input: CreateTaskComment): 3 | CreateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) 4 | deleteTaskComment(input: DeleteTaskComment): 5 | DeleteTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) 6 | updateTaskComment(input: UpdateTaskComment): 7 | UpdateTaskCommentPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) 8 | } 9 | 10 | input CreateTaskComment { 11 | taskID: UUID! 12 | message: String! 13 | } 14 | 15 | type CreateTaskCommentPayload { 16 | taskID: UUID! 17 | comment: TaskComment! 18 | } 19 | 20 | input UpdateTaskComment { 21 | commentID: UUID! 22 | message: String! 23 | } 24 | 25 | type UpdateTaskCommentPayload { 26 | taskID: UUID! 27 | comment: TaskComment! 28 | } 29 | 30 | input DeleteTaskComment { 31 | commentID: UUID! 32 | } 33 | 34 | type DeleteTaskCommentPayload { 35 | taskID: UUID! 36 | commentID: UUID! 37 | } 38 | -------------------------------------------------------------------------------- /internal/graph/schema/task/label.gql: -------------------------------------------------------------------------------- 1 | extend type Mutation { 2 | addTaskLabel(input: AddTaskLabelInput): 3 | Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) 4 | removeTaskLabel(input: RemoveTaskLabelInput): 5 | Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) 6 | toggleTaskLabel(input: ToggleTaskLabelInput!): 7 | ToggleTaskLabelPayload! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK) 8 | 9 | } 10 | 11 | input AddTaskLabelInput { 12 | taskID: UUID! 13 | projectLabelID: UUID! 14 | } 15 | 16 | input RemoveTaskLabelInput { 17 | taskID: UUID! 18 | taskLabelID: UUID! 19 | } 20 | 21 | input ToggleTaskLabelInput { 22 | taskID: UUID! 23 | projectLabelID: UUID! 24 | } 25 | 26 | type ToggleTaskLabelPayload { 27 | active: Boolean! 28 | task: Task! 29 | } 30 | -------------------------------------------------------------------------------- /internal/graph/schema/taskList.gql: -------------------------------------------------------------------------------- 1 | type TaskGroup { 2 | id: ID! 3 | projectID: String! 4 | createdAt: Time! 5 | name: String! 6 | position: Float! 7 | tasks: [Task!]! 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /internal/graph/schema/taskList/_model.gql: -------------------------------------------------------------------------------- 1 | type TaskGroup { 2 | id: ID! 3 | projectID: String! 4 | createdAt: Time! 5 | name: String! 6 | position: Float! 7 | tasks: [Task!]! 8 | } 9 | -------------------------------------------------------------------------------- /internal/graph/schema/taskList/taskList.gql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/internal/graph/schema/taskList/taskList.gql -------------------------------------------------------------------------------- /internal/graph/schema/team/_model.gql: -------------------------------------------------------------------------------- 1 | type Team { 2 | id: ID! 3 | createdAt: Time! 4 | name: String! 5 | permission: TeamPermission! 6 | members: [Member!]! 7 | } 8 | 9 | type TeamPermission { 10 | team: RoleCode! 11 | org: RoleCode! 12 | } 13 | -------------------------------------------------------------------------------- /internal/graph/schema/team/member.gql: -------------------------------------------------------------------------------- 1 | extend type Mutation { 2 | createTeamMember(input: CreateTeamMember!): 3 | CreateTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) 4 | updateTeamMemberRole(input: UpdateTeamMemberRole!): 5 | UpdateTeamMemberRolePayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) 6 | deleteTeamMember(input: DeleteTeamMember!): 7 | DeleteTeamMemberPayload! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM) 8 | 9 | } 10 | 11 | input DeleteTeamMember { 12 | teamID: UUID! 13 | userID: UUID! 14 | newOwnerID: UUID 15 | } 16 | 17 | type DeleteTeamMemberPayload { 18 | teamID: UUID! 19 | userID: UUID! 20 | affectedProjects: [Project!]! 21 | } 22 | 23 | input CreateTeamMember { 24 | userID: UUID! 25 | teamID: UUID! 26 | } 27 | 28 | type CreateTeamMemberPayload { 29 | team: Team! 30 | teamMember: Member! 31 | } 32 | 33 | input UpdateTeamMemberRole { 34 | teamID: UUID! 35 | userID: UUID! 36 | roleCode: RoleCode! 37 | } 38 | 39 | type UpdateTeamMemberRolePayload { 40 | ok: Boolean! 41 | teamID: UUID! 42 | member: Member! 43 | } 44 | -------------------------------------------------------------------------------- /internal/graph/schema/team/team.gql: -------------------------------------------------------------------------------- 1 | extend type Mutation { 2 | deleteTeam(input: DeleteTeam!): 3 | DeleteTeamPayload! @hasRole(roles:[ ADMIN], level: TEAM, type: TEAM) 4 | createTeam(input: NewTeam!): 5 | Team! @hasRole(roles: [ADMIN], level: ORG, type: ORG) 6 | } 7 | 8 | input NewTeam { 9 | name: String! 10 | organizationID: UUID! 11 | } 12 | 13 | input DeleteTeam { 14 | teamID: UUID! 15 | } 16 | 17 | type DeleteTeamPayload { 18 | ok: Boolean! 19 | team: Team! 20 | projects: [Project!]! 21 | } 22 | -------------------------------------------------------------------------------- /internal/graph/schema/user/_model.gql: -------------------------------------------------------------------------------- 1 | type UserAccount { 2 | id: ID! 3 | email: String! 4 | createdAt: Time! 5 | fullName: String! 6 | initials: String! 7 | bio: String! 8 | role: Role! 9 | username: String! 10 | profileIcon: ProfileIcon! 11 | owned: OwnedList! 12 | member: MemberList! 13 | } 14 | 15 | type InvitedUserAccount { 16 | id: ID! 17 | email: String! 18 | invitedOn: Time! 19 | member: MemberList! 20 | } 21 | -------------------------------------------------------------------------------- /internal/graph/taskList.resolvers.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/google/uuid" 10 | "github.com/jordanknott/taskcafe/internal/db" 11 | ) 12 | 13 | func (r *taskGroupResolver) ID(ctx context.Context, obj *db.TaskGroup) (uuid.UUID, error) { 14 | return obj.TaskGroupID, nil 15 | } 16 | 17 | func (r *taskGroupResolver) ProjectID(ctx context.Context, obj *db.TaskGroup) (string, error) { 18 | return obj.ProjectID.String(), nil 19 | } 20 | 21 | func (r *taskGroupResolver) Tasks(ctx context.Context, obj *db.TaskGroup) ([]db.Task, error) { 22 | tasks, err := r.Repository.GetTasksForTaskGroupID(ctx, obj.TaskGroupID) 23 | return tasks, err 24 | } 25 | 26 | // TaskGroup returns TaskGroupResolver implementation. 27 | func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} } 28 | 29 | type taskGroupResolver struct{ *Resolver } 30 | -------------------------------------------------------------------------------- /internal/jobs/logger.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | ) 6 | 7 | // MachineryLogger is a customer logger for machinery worker 8 | type MachineryLogger struct{} 9 | 10 | // Print sends to logrus.Info 11 | func (m *MachineryLogger) Print(args ...interface{}) { 12 | log.Info(args...) 13 | } 14 | 15 | // Printf sends to logrus.Infof 16 | func (m *MachineryLogger) Printf(format string, args ...interface{}) { 17 | log.Infof(format, args...) 18 | } 19 | 20 | // Println sends to logrus.Info 21 | func (m *MachineryLogger) Println(args ...interface{}) { 22 | log.Info(args...) 23 | } 24 | 25 | // Fatal sends to logrus.Fatal 26 | func (m *MachineryLogger) Fatal(args ...interface{}) { 27 | log.Fatal(args...) 28 | } 29 | 30 | // Fatalf sends to logrus.Fatalf 31 | func (m *MachineryLogger) Fatalf(format string, args ...interface{}) { 32 | log.Fatalf(format, args...) 33 | } 34 | 35 | // Fatalln sends to logrus.Fatal 36 | func (m *MachineryLogger) Fatalln(args ...interface{}) { 37 | log.Fatal(args...) 38 | } 39 | 40 | // Panic sends to logrus.Panic 41 | func (m *MachineryLogger) Panic(args ...interface{}) { 42 | log.Panic(args...) 43 | } 44 | 45 | // Panicf sends to logrus.Panic 46 | func (m *MachineryLogger) Panicf(format string, args ...interface{}) { 47 | log.Panic(args...) 48 | } 49 | 50 | // Panicln sends to logrus.Panic 51 | func (m *MachineryLogger) Panicln(args ...interface{}) { 52 | log.Panic(args...) 53 | } 54 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/uuid" 7 | "github.com/jordanknott/taskcafe/internal/utils" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // New returns a log entry with the reqID and userID fields populated if they exist 12 | func New(ctx context.Context) *log.Entry { 13 | entry := log.NewEntry(log.StandardLogger()) 14 | if reqID, ok := ctx.Value(utils.ReqIDKey).(uuid.UUID); ok { 15 | entry = entry.WithField("reqID", reqID) 16 | } 17 | if userID, ok := ctx.Value(utils.UserIDKey).(uuid.UUID); ok { 18 | entry = entry.WithField("userID", userID) 19 | } 20 | return entry 21 | } 22 | -------------------------------------------------------------------------------- /internal/route/log.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type ClientLog struct { 11 | Level string `json:"level"` 12 | Message string `json:"message"` 13 | Logger string `json:"logger"` 14 | Stacktrace string `json:"stacktrace"` 15 | Timestamp string `json:"timestamp"` 16 | } 17 | 18 | type ClientLogs struct { 19 | Logs []ClientLog `json:"logs"` 20 | } 21 | 22 | func (h *TaskcafeHandler) HandleClientLog(w http.ResponseWriter, r *http.Request) { 23 | var clientLogs ClientLogs 24 | err := json.NewDecoder(r.Body).Decode(&clientLogs) 25 | if err != nil { 26 | w.WriteHeader(http.StatusBadRequest) 27 | log.Debug("bad request body") 28 | return 29 | } 30 | for _, logEntry := range clientLogs.Logs { 31 | log.WithField("level", logEntry.Level).WithField("message", logEntry.Message).Info("found log") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/route/settings.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type PublicSettingsResponse struct { 11 | IsConfigured bool `json:"isConfigured"` 12 | AllowPublicRegistration bool `json:"allowPublicRegistration"` 13 | } 14 | 15 | func (h *TaskcafeHandler) PublicSettings(w http.ResponseWriter, r *http.Request) { 16 | userExists, err := h.repo.HasAnyUser(r.Context()) 17 | if err != nil { 18 | log.WithError(err).Error("issue checking if user accounts exist") 19 | w.WriteHeader(http.StatusInternalServerError) 20 | return 21 | } 22 | json.NewEncoder(w).Encode(PublicSettingsResponse{IsConfigured: userExists, AllowPublicRegistration: false}) 23 | } 24 | -------------------------------------------------------------------------------- /internal/utils/context.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // ContextKey represents a context key 4 | type ContextKey string 5 | 6 | const ( 7 | // UserIDKey is the key for the user id of the authenticated user 8 | UserIDKey ContextKey = "userID" 9 | // ReqIDKey is the unique ID key for current request 10 | ReqIDKey ContextKey = "reqID" 11 | //RestrictedModeKey is the key for whether the authenticated user only has access to install route 12 | RestrictedModeKey ContextKey = "restricted_mode" 13 | // OrgRoleKey is the key for the organization role code of the authenticated user 14 | OrgRoleKey ContextKey = "org_role" 15 | ) 16 | -------------------------------------------------------------------------------- /internal/utils/cursor.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | func DecodeCursor(encodedCursor string) (res time.Time, id uuid.UUID, err error) { 14 | byt, err := base64.StdEncoding.DecodeString(encodedCursor) 15 | if err != nil { 16 | return 17 | } 18 | 19 | arrStr := strings.Split(string(byt), ",") 20 | if len(arrStr) != 2 { 21 | err = errors.New("cursor is invalid") 22 | return 23 | } 24 | 25 | res, err = time.Parse(time.RFC3339Nano, arrStr[0]) 26 | if err != nil { 27 | return 28 | } 29 | id = uuid.MustParse(arrStr[1]) 30 | return 31 | } 32 | 33 | func EncodeCursor(t time.Time, id uuid.UUID) string { 34 | key := fmt.Sprintf("%s,%s", t.Format(time.RFC3339Nano), id.String()) 35 | return base64.StdEncoding.EncodeToString([]byte(key)) 36 | } 37 | -------------------------------------------------------------------------------- /internal/utils/redis.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type NotificationCreatedMessage struct { 4 | NotifiedID string 5 | NotificationID string 6 | } 7 | -------------------------------------------------------------------------------- /internal/utils/version.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | var ( 4 | version = "dev" 5 | commitHash = "none" 6 | buildDate = "unknown" 7 | ) 8 | 9 | type Info struct { 10 | Version string 11 | CommitHash string 12 | BuildDate string 13 | } 14 | 15 | func Version() Info { 16 | return Info{ 17 | Version: version, 18 | CommitHash: commitHash, 19 | BuildDate: buildDate, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/0001_add-refresh-token-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | 3 | CREATE TABLE refresh_token ( 4 | token_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 5 | user_id uuid NOT NULL, 6 | created_at timestamptz NOT NULL, 7 | expires_at timestamptz NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /migrations/0002_add-user_account-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user_account ( 2 | user_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | created_at timestamptz NOT NULL, 4 | first_name text NOT NULL, 5 | last_name text NOT NULL, 6 | email text NOT NULL UNIQUE, 7 | username text NOT NULL UNIQUE, 8 | password_hash text NOT NULL 9 | ); 10 | -------------------------------------------------------------------------------- /migrations/0003_add-team-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE team ( 2 | team_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | created_at timestamptz NOT NULL, 4 | name text NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /migrations/0004_add-project-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE project ( 2 | project_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | team_id uuid NOT NULL REFERENCES team(team_id), 4 | created_at timestamptz NOT NULL, 5 | name text NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/0005_add-task-group-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_group ( 2 | task_group_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | project_id uuid NOT NULL REFERENCES project(project_id), 4 | created_at timestamptz NOT NULL, 5 | name text NOT NULL, 6 | position float NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/0006_add-task.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task ( 2 | task_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | task_group_id uuid NOT NULL REFERENCES task_group(task_group_id), 4 | created_at timestamptz NOT NULL, 5 | name text NOT NULL, 6 | position float NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/0007_add-organization-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE organization ( 2 | organization_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | created_at timestamptz NOT NULL, 4 | name text NOT NULL 5 | ); 6 | 7 | INSERT INTO organization (created_at, name) VALUES (NOW(), 'sys_default_organization'); 8 | -------------------------------------------------------------------------------- /migrations/0008_add-org-id-to-team-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE team ADD COLUMN organization_id uuid NOT NULL REFERENCES organization(organization_id); 2 | -------------------------------------------------------------------------------- /migrations/0009_add-task-assigned-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_assigned ( 2 | task_assigned_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | task_id uuid NOT NULL REFERENCES task(task_id), 4 | user_id uuid NOT NULL REFERENCES user_account(user_id), 5 | assigned_date timestamptz NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/0010_add-description-to-task-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task ADD COLUMN description text; 2 | -------------------------------------------------------------------------------- /migrations/0011_add-label-color-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE label_color ( 2 | label_color_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | color_hex TEXT NOT NULL, 4 | position FLOAT NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /migrations/0012_add-project-label-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE project_label ( 2 | project_label_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | project_id uuid NOT NULL REFERENCES project(project_id), 4 | label_color_id uuid NOT NULL REFERENCES label_color(label_color_id), 5 | created_date timestamptz NOT NULL, 6 | name text 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/0013_add-due-date-to-task-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task ADD COLUMN due_date timestamptz; 2 | -------------------------------------------------------------------------------- /migrations/0014_add-owner-column-to-project-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project ADD COLUMN owner uuid NOT NULL; 2 | -------------------------------------------------------------------------------- /migrations/0015_add-task-label-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_label ( 2 | task_label_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | task_id uuid NOT NULL REFERENCES task(task_id), 4 | project_label_id uuid NOT NULL REFERENCES project_label(project_label_id), 5 | assigned_date timestamptz NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/0016_add-profile_bg_color-column-to-user-account-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ADD COLUMN profile_bg_color text NOT NULL DEFAULT '#7367F0'; 2 | -------------------------------------------------------------------------------- /migrations/0017_add-profile-bg-delete-cascade.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_assigned 2 | DROP CONSTRAINT task_assigned_task_id_fkey, 3 | ADD CONSTRAINT task_assigned_task_id_fkey 4 | FOREIGN KEY (task_id) 5 | REFERENCES task(task_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0018_add-name-column-to-label-color.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE label_color ADD COLUMN name TEXT NOT NULL DEFAULT 'needs name'; 2 | -------------------------------------------------------------------------------- /migrations/0019_add-unique-constraint-project-label-and-task-on-task-label.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_label ADD UNIQUE (project_label_id, task_id); 2 | -------------------------------------------------------------------------------- /migrations/0020_add-full-name-column-to-user_account-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ADD COLUMN full_name TEXT; 2 | UPDATE user_account SET full_name = CONCAT(first_name, ' ', last_name); 3 | ALTER TABLE user_account ALTER COLUMN full_name SET NOT NULL; 4 | -------------------------------------------------------------------------------- /migrations/0021_drop-first-name-column-from-user_account-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account DROP COLUMN first_name; 2 | -------------------------------------------------------------------------------- /migrations/0022_drop-last-name-column-from-user_account-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account DROP COLUMN last_name; 2 | -------------------------------------------------------------------------------- /migrations/0023_add-initials-column-to-user_account-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ADD COLUMN initials TEXT NOT NULL DEFAULT ''; 2 | -------------------------------------------------------------------------------- /migrations/0024_add-profile-avatar-url-column-to-user_account-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ADD COLUMN profile_avatar_url TEXT; 2 | -------------------------------------------------------------------------------- /migrations/0025_add-cascade-delete-to-task-id-fk-on-task_label.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_label DROP CONSTRAINT task_label_task_id_fkey; 2 | ALTER TABLE task_label 3 | ADD CONSTRAINT task_label_task_id_fkey 4 | FOREIGN KEY (task_id) 5 | REFERENCES task(task_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0026_add-cascade-delete-to-task-id-fk-on-task_assigned.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_assigned DROP CONSTRAINT task_assigned_task_id_fkey; 2 | ALTER TABLE task_assigned 3 | ADD CONSTRAINT task_assigned_task_id_fkey 4 | FOREIGN KEY (task_id) 5 | REFERENCES task(task_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0027_add-cascade-delete-to-task_group_id-fk-on-task.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task DROP CONSTRAINT task_task_group_id_fkey; 2 | ALTER TABLE task 3 | ADD CONSTRAINT task_task_group_id_fkey 4 | FOREIGN KEY (task_group_id) 5 | REFERENCES task_group(task_group_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0028_add-complete-column-to-task-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task ADD COLUMN complete boolean NOT NULL DEFAULT FALSE; 2 | -------------------------------------------------------------------------------- /migrations/0029_add-task_checklist.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_checklist ( 2 | task_checklist_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | task_id uuid NOT NULL REFERENCES task(task_id) ON DELETE CASCADE, 4 | created_at timestamptz NOT NULL, 5 | name text NOT NULL, 6 | position float NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/0030_add-task_checklist_item.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_checklist_item ( 2 | task_checklist_item_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | task_checklist_id uuid NOT NULL REFERENCES task_checklist(task_checklist_id) ON DELETE CASCADE, 4 | created_at timestamptz NOT NULL, 5 | complete boolean NOT NULL DEFAULT false, 6 | name text NOT NULL, 7 | position float NOT NULL, 8 | due_date timestamptz 9 | ); 10 | -------------------------------------------------------------------------------- /migrations/0031_add-team-member-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE team_member ( 2 | team_member_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | team_id uuid NOT NULL REFERENCES team(team_id) ON DELETE CASCADE, 4 | user_id uuid NOT NULL REFERENCES user_account(user_id) ON DELETE CASCADE, 5 | UNIQUE(team_id, user_id), 6 | addedDate timestamptz NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/0032_add-cascade-delete-to-project_label.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project_label DROP CONSTRAINT project_label_project_id_fkey; 2 | ALTER TABLE project_label 3 | ADD CONSTRAINT project_label_project_id_fkey 4 | FOREIGN KEY (project_id) 5 | REFERENCES project(project_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0033_add-cascade-delete-to_task_label_project_label_id_fkey.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_label DROP CONSTRAINT task_label_project_label_id_fkey; 2 | ALTER TABLE task_label 3 | ADD CONSTRAINT task_label_project_label_id_fkey 4 | FOREIGN KEY (project_label_id) 5 | REFERENCES project_label(project_label_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0034_add-cascade-delete-to_project_team_id_fkey.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project DROP CONSTRAINT project_team_id_fkey; 2 | ALTER TABLE project 3 | ADD CONSTRAINT project_team_id_fkey 4 | FOREIGN KEY (team_id) 5 | REFERENCES team(team_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0035_add-cascade-delete-to-task_assigned_user_id_fkey.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_assigned DROP CONSTRAINT task_assigned_user_id_fkey; 2 | ALTER TABLE task_assigned 3 | ADD CONSTRAINT task_assigned_user_id_fkey 4 | FOREIGN KEY (user_id) 5 | REFERENCES user_account(user_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0036_add-fkey-to-user_id-on-refresh_token-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE refresh_token 2 | ADD CONSTRAINT refresh_token_user_id_fkey 3 | FOREIGN KEY (user_id) 4 | REFERENCES user_account(user_id) 5 | ON DELETE CASCADE; 6 | -------------------------------------------------------------------------------- /migrations/0037_add-project_member-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE project_member ( 2 | project_member_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | project_id uuid NOT NULL REFERENCES project(project_id) ON DELETE CASCADE, 4 | user_id uuid NOT NULL REFERENCES user_account(user_id) ON DELETE CASCADE, 5 | added_at timestamptz NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/0038_add-role-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE role ( 2 | code TEXT PRIMARY KEY, 3 | name TEXT NOT NULL 4 | ); 5 | 6 | INSERT INTO role VALUES ('owner', 'Owner'); 7 | INSERT INTO role VALUES ('admin', 'Admin'); 8 | INSERT INTO role VALUES ('member', 'Member'); 9 | INSERT INTO role VALUES ('observer', 'Observer'); 10 | -------------------------------------------------------------------------------- /migrations/0039_add-role-column-to-user_account-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ADD COLUMN role_code text 2 | NOT NULL REFERENCES role(code) ON DELETE CASCADE DEFAULT 'member'; 3 | -------------------------------------------------------------------------------- /migrations/0040_add-unique-constraint-to-project_member-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project_member ADD UNIQUE (project_id, user_id); 2 | -------------------------------------------------------------------------------- /migrations/0041_add-role_code-column-to-project_member-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project_member ADD COLUMN role_code TEXT 2 | NOT NULL REFERENCES role(code) ON DELETE CASCADE DEFAULT 'member'; 3 | -------------------------------------------------------------------------------- /migrations/0042_add-default-null-to-profile_avatar_url-on-user_account.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ALTER COLUMN profile_avatar_url SET DEFAULT null; 2 | -------------------------------------------------------------------------------- /migrations/0043_add-role_code-column-to-team_member-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE team_member ADD COLUMN role_code TEXT NOT NULL REFERENCES role(code) ON DELETE CASCADE; 2 | -------------------------------------------------------------------------------- /migrations/0044_add-owner-to-team-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE team ADD COLUMN owner uuid REFERENCES user_account(user_id) ON DELETE 2 | CASCADE; 3 | UPDATE team SET owner = (SELECT user_id FROM user_account WHERE role_code = 'admin' LIMIT 1); 4 | ALTER TABLE team ALTER COLUMN owner SET NOT NULL; 5 | -------------------------------------------------------------------------------- /migrations/0045_add-system_options-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE system_options ( 2 | option_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | key text NOT NULL UNIQUE, 4 | value text 5 | ); 6 | -------------------------------------------------------------------------------- /migrations/0046_add-system-user.up.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO user_account(created_at, email, initials, username, full_name, 2 | role_code, password_hash) VALUES (NOW(), '', 'SYS', 'system', 'System', 'owner', ''); 3 | -------------------------------------------------------------------------------- /migrations/0047_add-default-label_colors.up.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO label_color (color_hex, position, name ) VALUES ( 'transparent', 0.0, 'no_color' ); 2 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#e8384f', 1.0, 'red' ); 3 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#fd612c', 2.0, 'orange' ); 4 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#fd9a00', 3.0, 'yellow_orange' ); 5 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#eec300', 4.0, 'yellow' ); 6 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#a4cf30', 5.0, 'yellow_green' ); 7 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#62d26f', 6.0, 'green' ); 8 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#37c5ab', 6.0, 'blue_green' ); 9 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#20aaea', 6.0, 'aqua' ); 10 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#4186e0', 6.0, 'blue' ); 11 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#7a6ff0', 6.0, 'indigo' ); 12 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#aa62e3', 6.0, 'purple' ); 13 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#e362e3', 6.0, 'magenta' ); 14 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#ea4e9d', 6.0, 'hot_pink' ); 15 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#fc91ad', 6.0, 'pink' ); 16 | INSERT INTO label_color (color_hex, position, name ) VALUES ( '#8da3a6', 6.0, 'cool_gray' ); 17 | -------------------------------------------------------------------------------- /migrations/0048_add-cascade-delete-to-task_group_project_id_fkey.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_group DROP CONSTRAINT task_group_project_id_fkey; 2 | ALTER TABLE task_group 3 | ADD CONSTRAINT task_group_project_id_fkey 4 | FOREIGN KEY (project_id) 5 | REFERENCES project(project_id) 6 | ON DELETE CASCADE; 7 | -------------------------------------------------------------------------------- /migrations/0049_remove-owner-column-from-project-table.up.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO project_member (user_id, project_id, added_at, role_code) 2 | SELECT owner as user_id, project_id, NOW() as added_at, 'admin' as role_code 3 | FROM project; 4 | ALTER TABLE project DROP COLUMN owner; 5 | -------------------------------------------------------------------------------- /migrations/0050_remove-owner-column-from-team-table.up.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO team_member (user_id, team_id, addeddate, role_code) 2 | SELECT owner as user_id, team_id, NOW() as addeddate, 'admin' as role_code 3 | FROM team ON CONFLICT ON CONSTRAINT team_member_team_id_user_id_key DO NOTHING; 4 | ALTER TABLE team DROP COLUMN owner; 5 | -------------------------------------------------------------------------------- /migrations/0051_add-completed_at-to-task-table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task ADD COLUMN completed_at timestamptz; 2 | UPDATE task as t1 SET completed_at = NOW() 3 | FROM task as t2 4 | WHERE t1.task_id = t2.task_id AND t1.complete = true; 5 | -------------------------------------------------------------------------------- /migrations/0052_add-bio-col-to-user_account.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ADD COLUMN bio text NOT NULL DEFAULT ''; 2 | -------------------------------------------------------------------------------- /migrations/0053_add-notification-tables.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE notification_object ( 2 | notification_object_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | entity_id uuid NOT NULL, 4 | action_type int NOT NULL, 5 | actor_id uuid NOT NULL, 6 | entity_type int NOT NULL, 7 | created_on timestamptz NOT NULL 8 | ); 9 | 10 | CREATE TABLE notification ( 11 | notification_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 12 | notification_object_id uuid REFERENCES notification_object(notification_object_id) ON DELETE CASCADE, 13 | notifier_id uuid NOT NULL REFERENCES user_account(user_id) ON DELETE CASCADE, 14 | read boolean NOT NULL DEFAULT false 15 | ); 16 | -------------------------------------------------------------------------------- /migrations/0054_remove-team-not-null-constraint-projects.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project ALTER COLUMN team_id DROP NOT NULL; 2 | -------------------------------------------------------------------------------- /migrations/0055_add-personal_project_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE personal_project ( 2 | personal_project_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | project_id uuid NOT NULL REFERENCES project(project_id) ON DELETE CASCADE, 4 | user_id uuid NOT NULL REFERENCES user_account(user_id) ON DELETE CASCADE 5 | ); 6 | -------------------------------------------------------------------------------- /migrations/0056_add-user_account_invited-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user_account_invited ( 2 | user_account_invited_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | email text NOT NULL UNIQUE, 4 | invited_on timestamptz NOT NULL DEFAULT NOW(), 5 | has_joined boolean NOT NULL DEFAULT false 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/0057_add-project_member_invited-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE project_member_invited ( 2 | project_member_invited_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | project_id uuid NOT NULL 4 | REFERENCES project(project_id) ON DELETE CASCADE, 5 | user_account_invited_id uuid NOT NULL 6 | REFERENCES user_account_invited(user_account_invited_id) ON DELETE CASCADE 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/0058_add-active-column-to-user_account.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_account ADD COLUMN active boolean NOT NULL DEFAULT false; 2 | UPDATE user_account SET active = true; 3 | -------------------------------------------------------------------------------- /migrations/0059_add-confirm_token_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user_account_confirm_token ( 2 | confirm_token_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | email text NOT NULL UNIQUE 4 | ); 5 | -------------------------------------------------------------------------------- /migrations/0060_add-task_activity-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_activity_type ( 2 | task_activity_type_id int PRIMARY KEY, 3 | code text NOT NULL, 4 | template text NOT NULL 5 | ); 6 | 7 | INSERT INTO task_activity_type (task_activity_type_id, code, template) VALUES 8 | (1, 'task_added_to_task_group', 'added this task to {{ index .Data "TaskGroup" }}'), 9 | (2, 'task_moved_to_task_group', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'), 10 | (3, 'task_mark_complete', 'marked this task complete'), 11 | (4, 'task_mark_incomplete', 'marked this task incomplete'), 12 | (5, 'task_due_date_changed', 'changed the due date to {{ index .Data "DueDate" }}'), 13 | (6, 'task_due_date_added', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'), 14 | (7, 'task_due_date_removed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'), 15 | (8, 'task_checklist_changed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'), 16 | (9, 'task_checklist_added', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'), 17 | (10, 'task_checklist_removed', 'moved this task from {{ index .Data "PrevTaskGroup" }} to {{ index .Data "CurTaskGroup"}}'); 18 | 19 | CREATE TABLE task_activity ( 20 | task_activity_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 21 | active boolean NOT NULL DEFAULT true, 22 | task_id uuid NOT NULL REFERENCES task(task_id), 23 | created_at timestamptz NOT NULL, 24 | caused_by uuid NOT NULL, 25 | activity_type_id int NOT NULL REFERENCES task_activity_type(task_activity_type_id), 26 | data jsonb 27 | ); 28 | -------------------------------------------------------------------------------- /migrations/0061_add-task_comment-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_comment ( 2 | task_comment_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | task_id uuid NOT NULL REFERENCES task(task_id), 4 | created_at timestamptz NOT NULL, 5 | updated_at timestamptz, 6 | created_by uuid NOT NULL REFERENCES user_account(user_id), 7 | pinned boolean NOT NULL DEFAULT false, 8 | message TEXT NOT NULL 9 | ); 10 | -------------------------------------------------------------------------------- /migrations/0062_add-cascade-delete-to-task-comments-and-activity.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_activity DROP CONSTRAINT task_activity_task_id_fkey; 2 | ALTER TABLE task_activity 3 | ADD CONSTRAINT task_activity_task_id_fkey 4 | FOREIGN KEY (task_id) 5 | REFERENCES task(task_id) 6 | ON DELETE CASCADE; 7 | 8 | ALTER TABLE task_comment DROP CONSTRAINT task_comment_task_id_fkey; 9 | ALTER TABLE task_comment 10 | ADD CONSTRAINT task_comment_task_id_fkey 11 | FOREIGN KEY (task_id) 12 | REFERENCES task(task_id) 13 | ON DELETE CASCADE; 14 | -------------------------------------------------------------------------------- /migrations/0063_add-use_time-to-task-due_date.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task ADD COLUMN has_time boolean NOT NULL DEFAULT false; 2 | UPDATE task SET has_time = true; 3 | -------------------------------------------------------------------------------- /migrations/0064_rename-refresh_token-to-auth_token.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE refresh_token RENAME TO auth_token; 2 | -------------------------------------------------------------------------------- /migrations/0065_add-public_on-to-project.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project ADD COLUMN public_on timestamptz; 2 | -------------------------------------------------------------------------------- /migrations/0066_redesign-notification-table.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE notification_object CASCADE; 2 | DROP TABLE notification CASCADE; 3 | 4 | CREATE TABLE notification ( 5 | notification_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 6 | caused_by uuid NOT NULL, 7 | action_type text NOT NULL, 8 | data jsonb, 9 | created_on timestamptz NOT NULL 10 | ); 11 | 12 | CREATE TABLE notification_notified ( 13 | notified_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 14 | notification_id uuid REFERENCES notification(notification_id) ON DELETE CASCADE, 15 | user_id uuid NOT NULL REFERENCES user_account(user_id) ON DELETE CASCADE, 16 | read boolean NOT NULL DEFAULT false, 17 | read_at timestamptz 18 | ); 19 | 20 | CREATE INDEX idx_notification_pagination ON notification (created_on, notification_id); 21 | -------------------------------------------------------------------------------- /migrations/0068_add-task_watcher-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_watcher ( 2 | task_watcher_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | task_id uuid NOT NULL REFERENCES task(task_id) ON DELETE CASCADE, 4 | user_id uuid NOT NULL REFERENCES user_account(user_id) ON DELETE CASCADE, 5 | watched_at timestamptz NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/0070_add-task_due_date_notification.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE task_due_date_reminder_duration ( 2 | code text PRIMARY KEY 3 | ); 4 | 5 | INSERT INTO task_due_date_reminder_duration VALUES ('MINUTE'); 6 | INSERT INTO task_due_date_reminder_duration VALUES ('HOUR'); 7 | INSERT INTO task_due_date_reminder_duration VALUES ('DAY'); 8 | INSERT INTO task_due_date_reminder_duration VALUES ('WEEK'); 9 | 10 | CREATE TABLE task_due_date_reminder ( 11 | due_date_reminder_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 12 | task_id uuid NOT NULL REFERENCES task(task_id) ON DELETE CASCADE, 13 | period int NOT NULL, 14 | duration text NOT NULL REFERENCES task_due_date_reminder_duration(code) ON DELETE CASCADE 15 | ); 16 | -------------------------------------------------------------------------------- /migrations/0071_add-task_due_date_notification-at-col.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE task_due_date_reminder ADD COLUMN remind_at timestamptz NOT NULL DEFAULT NOW(); 2 | -------------------------------------------------------------------------------- /migrations/0072_remove-not-null-from-notification-caused_by.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE notification ALTER COLUMN caused_by DROP NOT NULL; 2 | UPDATE notification SET caused_by = null WHERE caused_by = '00000000-0000-0000-0000-000000000000'; 3 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PASS=true 4 | for FILE in "$@" 5 | do 6 | yarn --cwd frontend run eslint $(echo $FILE | sed 's/frontend\///g') 7 | if [ "$?" -eq 1 ]; then 8 | PASS=false 9 | fi 10 | done 11 | 12 | if [ "$PASS" = "false" ]; then 13 | exit 1 14 | fi 15 | -------------------------------------------------------------------------------- /sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "1" 2 | packages: 3 | - name: "db" 4 | emit_json_tags: true 5 | emit_prepared_queries: false 6 | emit_interface: true 7 | path: "internal/db" 8 | queries: "./internal/db/query/" 9 | schema: "./migrations/" 10 | -------------------------------------------------------------------------------- /templates/mail/user/registered.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.Username}} has been registered 6 | 7 | 8 | 9 |

Hi {{.Username}}, thanks for registering at {{.AppName}}!

10 |

© {{.Year}} {{.AppName}}

11 | 12 | 13 | -------------------------------------------------------------------------------- /testing/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | build: ../ 5 | ports: 6 | - "6677:3333" 7 | depends_on: 8 | - postgres 9 | networks: 10 | - taskcafe-dev-test 11 | environment: 12 | TASKCAFE_DATABASE_HOST: postgres 13 | TASKCAFE_MIGRATE: "true" 14 | postgres: 15 | image: postgres:12.3-alpine 16 | restart: always 17 | networks: 18 | - taskcafe-dev-test 19 | environment: 20 | POSTGRES_USER: taskcafe 21 | POSTGRES_PASSWORD: taskcafe_test 22 | POSTGRES_DB: taskcafe 23 | 24 | networks: 25 | taskcafe-dev-test: 26 | driver: bridge 27 | -------------------------------------------------------------------------------- /testing/docker-compose.latest.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | image: taskcafe/taskcafe:latest 5 | # build: . 6 | ports: 7 | - "6688:3333" 8 | depends_on: 9 | - postgres 10 | networks: 11 | - taskcafe-latest-test 12 | environment: 13 | TASKCAFE_DATABASE_HOST: postgres 14 | TASKCAFE_MIGRATE: "true" 15 | postgres: 16 | image: postgres:12.3-alpine 17 | restart: always 18 | networks: 19 | - taskcafe-latest-test 20 | environment: 21 | POSTGRES_USER: taskcafe 22 | POSTGRES_PASSWORD: taskcafe_test 23 | POSTGRES_DB: taskcafe 24 | volumes: 25 | - taskcafe-latest-postgres:/var/lib/postgresql/data 26 | 27 | volumes: 28 | taskcafe-latest-postgres: 29 | external: false 30 | 31 | networks: 32 | taskcafe-latest-test: 33 | driver: bridge 34 | -------------------------------------------------------------------------------- /uploads/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JordanKnott/taskcafe/998db2a5da497a7902198a728ddc040fcacbacd6/uploads/.keep --------------------------------------------------------------------------------