diff --git a/.github/workflows/amplify-deployment.yml b/.github/workflows/amplify-deployment.yml new file mode 100644 index 0000000..bd4a453 --- /dev/null +++ b/.github/workflows/amplify-deployment.yml @@ -0,0 +1,110 @@ +name: Set Amplify Environment Variables and Trigger Deployment + +permissions: + id-token: write + contents: read + +on: + push: + branches: + - main + paths: + - "tasker-client/**" + workflow_dispatch: # Manual trigger + +env: + APPLICATION_NAME: tasker + +jobs: + deploy-amplify: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Verify AWS CLI Installation + run: aws --version + + - name: Install Amplify CLI + run: npm install -g @aws-amplify/cli + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Fetch Environment Variables from AWS SSM + run: | + export NEXT_PUBLIC_API_BASE_URL=$(aws ssm get-parameter --name "/tasker/api/base-url" --query "Parameter.Value" --output text || exit 1) + export NEXT_PUBLIC_COGNITO_USER_POOL_ID=$(aws ssm get-parameter --name "/tasker/cognito/user-pool-id" --query "Parameter.Value" --output text || exit 1) + export NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID=$(aws ssm get-parameter --name "/tasker/cognito/client-id" --query "Parameter.Value" --output text || exit 1) + export NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL=$(aws ssm get-parameter --name "/tasker/s3/public-images-url" --query "Parameter.Value" --output text || exit 1) + + echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL" >> $GITHUB_ENV + echo "NEXT_PUBLIC_COGNITO_USER_POOL_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_ID" >> $GITHUB_ENV + echo "NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID" >> $GITHUB_ENV + echo "NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL=$NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL" >> $GITHUB_ENV + + - name: Load Build Spec from `tasker-client/amplify.json` + run: | + build_spec=$(jq -c . tasker-client/amplify.json) + echo "build_spec=$build_spec" >> $GITHUB_ENV + + - name: Initialize or Update Amplify App + id: amplify + run: | + echo "Fetching Amplify apps in region ${{ secrets.AWS_REGION }}..." + app_list=$(aws amplify list-apps --region ${{ secrets.AWS_REGION }}) + app_id=$(echo "$app_list" | jq -r '.apps[] | select(.name == "${{ env.APPLICATION_NAME }}") | .appId') + + if [ -n "$app_id" ]; then + echo "Amplify App already exists. Updating..." + echo "app_id=$app_id" >> $GITHUB_ENV + aws amplify update-app \ + --app-id "$app_id" \ + --environment-variables "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL,NEXT_PUBLIC_COGNITO_USER_POOL_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_ID,NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID,NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL=$NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL,AMPLIFY_MONOREPO_APP_ROOT=tasker-client,AMPLIFY_DIFF_DEPLOY=false" \ + --build-spec "$build_spec" + else + echo "Creating a new Amplify app..." + formatted_repo_url=$(echo "${{ github.repositoryUrl }}" | sed 's|git://|https://|') + create_app=$(aws amplify create-app \ + --name "${{ env.APPLICATION_NAME }}" \ + --platform WEB_COMPUTE \ + --repository "$formatted_repo_url" \ + --environment-variables "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL,NEXT_PUBLIC_COGNITO_USER_POOL_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_ID,NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID=$NEXT_PUBLIC_COGNITO_USER_POOL_CLIENT_ID,NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL=$NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL,AMPLIFY_MONOREPO_APP_ROOT=tasker-client,AMPLIFY_DIFF_DEPLOY=false" \ + --iam-service-role-arn "${{ secrets.AWS_ROLE_TO_ASSUME }}" \ + --region "${{ secrets.AWS_REGION }}" \ + --access-token "${{ secrets.PAT_TOKEN }}" \ + --build-spec "$build_spec" \ + --output "json" + ) + app_id=$(echo $create_app | jq -r '.app.appId') + echo "app_id=$app_id" >> $GITHUB_ENV + fi + + - name: Create or Update Amplify Branch + run: | + echo "Checking if branch ${{ github.ref_name}} exists..." + branch_exists=$(aws amplify list-branches \ + --app-id "$app_id" \ + --query "branches[?branchName=='${{ github.ref_name}}'] | length(@)" \ + --output text) + + if [ "$branch_exists" -gt 0 ]; then + echo "Branch ${{ github.ref_name}} already exists. Skipping..." + else + echo "Branch ${{ github.ref_name}} does not exist. Creating it..." + aws amplify create-branch \ + --app-id "$app_id" \ + --branch-name "${{ github.ref_name}}" + fi + + - name: Trigger Amplify Deployment + run: | + echo "Triggering deployment for branch ${{ github.ref_name}}..." + aws amplify start-job \ + --app-id "$app_id" \ + --branch-name "${{ github.ref_name}}" \ + --job-type RELEASE diff --git a/.github/workflows/serverless-deployment.yml b/.github/workflows/serverless-deployment.yml new file mode 100644 index 0000000..f48ac79 --- /dev/null +++ b/.github/workflows/serverless-deployment.yml @@ -0,0 +1,45 @@ +name: Deploy with Serverless Framework + +permissions: + id-token: write + contents: read + +on: + push: + branches: + - main + paths: + - "tasker-server/**" + workflow_dispatch: # This is a manual trigger + +jobs: + deploy-serverless: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Dependencies + working-directory: tasker-server + run: npm ci + + - name: Run Tests + working-directory: tasker-server + run: npm run test:ci + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Deploy with Serverless GitHub Action + uses: serverless/github-action@v3.2 + with: + args: -c "cd ./tasker-server && serverless deploy" + entrypoint: /bin/sh diff --git a/tasker-client/amplify.json b/tasker-client/amplify.json new file mode 100644 index 0000000..e648917 --- /dev/null +++ b/tasker-client/amplify.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "applications": [ + { + "appRoot": "tasker-client", + "frontend": { + "phases": { + "preBuild": { + "commands": [ + "npm ci" + ] + }, + "build": { + "commands": [ + "npm run build" + ] + } + }, + "artifacts": { + "baseDirectory": ".next", + "files": [ + "**/*" + ] + }, + "cache": { + "paths": [ + "node_modules/**/*", + ".next/cache/**/*", + ".npm/**/*" + ] + } + } + } + ] +} diff --git a/tasker-client/next.config.ts b/tasker-client/next.config.ts index e9ffa30..eede2b9 100644 --- a/tasker-client/next.config.ts +++ b/tasker-client/next.config.ts @@ -1,7 +1,17 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { + protocol: "https", + hostname: new URL(process.env.NEXT_PUBLIC_S3_PUBLIC_IMAGE_URL || "") + .hostname, + port: "", + pathname: "/**", + }, + ], + }, }; export default nextConfig; diff --git a/tasker-client/src/app/components/ModalNewTask/index.tsx b/tasker-client/src/app/components/ModalNewTask/index.tsx index 9748383..0631291 100644 --- a/tasker-client/src/app/components/ModalNewTask/index.tsx +++ b/tasker-client/src/app/components/ModalNewTask/index.tsx @@ -1,6 +1,11 @@ import Modal from "@/app/components/Modal"; -import { Priority, Status, useCreateTaskMutation } from "@/state/api"; -import React, { useState } from "react"; +import { + Priority, + Status, + useCreateTaskMutation, + useGetAuthUserQuery, +} from "@/state/api"; +import React, { useEffect, useState } from "react"; import { formatISO } from "date-fns"; type Props = { @@ -22,22 +27,28 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => { const [assignedUserId, setAssignedUserId] = useState(""); const [projectId, setProjectId] = useState(""); + const { data: currentUser } = useGetAuthUserQuery({}); + const userId = currentUser?.userDetails?.userId ?? null; + + useEffect(() => { + setAuthorUserId(userId || ""); + }, [userId]); + const handleSubmit = async () => { - console.log(title, authorUserId, id, projectId); + if (!(title && authorUserId && (id !== null || projectId))) return; - console.log("Creating task 1.."); - if ( - !(title && authorUserId && assignedUserId && (id !== null || projectId)) - ) - return; - console.log("Creating task 2..."); + const finalAssignedUserId = assignedUserId.trim() || authorUserId; - const formattedStartDate = formatISO(new Date(startDate), { - representation: "complete", - }); - const formattedDueDate = formatISO(new Date(dueDate), { - representation: "complete", - }); + const formattedStartDate = + startDate ?? + formatISO(new Date(startDate), { + representation: "complete", + }); + const formattedDueDate = + dueDate ?? + formatISO(new Date(dueDate), { + representation: "complete", + }); await createTask({ title, @@ -48,16 +59,15 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => { startDate: formattedStartDate, dueDate: formattedDueDate, authorUserId: authorUserId, - assignedUserId: assignedUserId, + assignedUserId: finalAssignedUserId, projectId: id !== null ? id : projectId, }); + + onClose(); }; const isFormValid = () => { - console.log(title, authorUserId, id, projectId); - return ( - title && authorUserId && assignedUserId && (id !== null || projectId) - ); + return title && authorUserId && (id !== null || projectId); }; const selectStyles = @@ -92,9 +102,13 @@ const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => { setAuthorUserId(e.target.value)} - /> + {authorUserId === "" && ( + setAuthorUserId(e.target.value)} + /> + )} {
{!!currentUserDetails?.profilePictureUrl ? ( {currentUserDetails?.username {
logo {
{!!currentUserDetails?.profilePictureUrl ? ( {currentUserDetails?.username {
{task.attachments && task.attachments.length > 0 && ( {task.attachments[0].fileName} {
{user.profilePictureUrl && ( profile picture { data: fetchedTasks, isLoading, error, + refetch, } = useGetTasksQuery({ projectId }); const [updateTaskStatus] = useUpdateTaskStatusMutation(); const [tasks, setTasks] = useState([]); @@ -43,6 +44,7 @@ const BoardView = ({ projectId, setIsModalNewTaskOpen }: BoardProps) => { try { await updateTaskStatus({ taskId, status: toStatus }); + await refetch(); } catch (error) { console.error("Failed to update task status:", error); setTasks(fetchedTasks || []); @@ -197,7 +199,7 @@ const Task = ({ task }: TaskProps) => { > {task.attachments && task.attachments.length > 0 && ( {task.attachments[0].fileName} {
{task.assignee && ( {task.assignee.username} { )} {task.author && ( {task.author.username} { }); const ganttTasks = useMemo(() => { + if (!tasks || tasks.length === 0) return []; return ( - tasks?.map((task) => ({ - start: new Date(task.startDate as string), - end: new Date(task.dueDate as string), - name: task.title, - id: `Task-${task.taskId}`, - type: "task" as TaskTypeItems, - progress: task.points ? (task.points / 10) * 100 : 0, - isDisabled: false, - })) || [] + tasks + ?.filter((task) => task.startDate && task.dueDate) + .map((task) => ({ + start: new Date(task.startDate as string), + end: new Date(task.dueDate as string), + name: task.title, + id: `Task-${task.taskId}`, + type: "task" as TaskTypeItems, + progress: task.points ? (task.points / 10) * 100 : 0, + isDisabled: false, + })) || [] ); }, [tasks]); @@ -66,16 +69,20 @@ const Timeline = ({ projectId, setIsModalNewTaskOpen }: Props) => {
-
- -
+ {ganttTasks.length > 0 && ( +
+ +
+ )}