feat: automate prod and add filter options, and date picker (#355)
* feat: automate prod deployments issue #338 * fix: string parse * feat: add date picker and filter options issue #127 * feat: add tooltip and date picker * ci: split stage deploy into separate tasks to allow separate releases * test: add more test cases * ci: split workflows into client and server * ci: fix pr server workflow
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
name: Main Client
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'client/**'
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '*.yml'
|
||||
|
||||
jobs:
|
||||
client-code-checks:
|
||||
name: Client Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Clean and Install
|
||||
run: cd client && npm ci
|
||||
- name: Build
|
||||
run: cd client && npm run build
|
||||
- name: Lint
|
||||
run: cd client && npm run lint
|
||||
- name: Unit tests
|
||||
run: cd client && npm run test:coverage
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5.0.0
|
||||
with:
|
||||
projectBaseDir: client
|
||||
args: >
|
||||
-Dsonar.organization=ricardo-campos-org
|
||||
-Dsonar.projectKey=ricardo-campos-org_react-typescript-todolist_client
|
||||
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
-Dsonar.typescript.tsconfigPaths=tsconfig.json
|
||||
-Dsonar.sources=src/
|
||||
-Dsonar.exclusions=src/__test__/**
|
||||
-Dsonar.tests=src/__test__/
|
||||
-Dsonar.verbose=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
image-promotion-and-deploy:
|
||||
name: Image promotion and deployment
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DEPLOY_DOMAIN: ${{ vars.DEPLOY_DOMAIN }}
|
||||
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
|
||||
GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }}
|
||||
API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
CLIENT_APPID: ${{ secrets.PROD_WEB_CLIENT_ID }}
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
name: Login to GitHub Container Registry
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull image with source tag
|
||||
run: docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate
|
||||
|
||||
- name: Extract PR number using docker inspect
|
||||
id: inspect
|
||||
run: |
|
||||
SOURCE_PR=$(docker inspect ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p')
|
||||
echo "SOURCE_PR=$SOURCE_PR" >> $GITHUB_ENV
|
||||
echo "source_pr=$SOURCE_PR" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Re-tag the image
|
||||
run: docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/client:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
|
||||
- name: Push new tag
|
||||
run: docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/client:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
|
||||
- name: Update image tag to be deployed
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
response=$(curl -X POST \
|
||||
"'${DEPLOY_DOMAIN}/api/application.saveDockerProvider'" \
|
||||
--max-time 30 \
|
||||
-H "accept: application/json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d '{
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/client:prod-${{ steps.inspect.outputs.source_pr }}",
|
||||
"applicationId": "${CLIENT_APPID}",
|
||||
"username": "${GHCR_USERNAME}",
|
||||
"password": "${GHCR_PASSWORD}",
|
||||
"registryUrl": "ghcr.io"
|
||||
}' \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
echo "Status code: $status_code"
|
||||
|
||||
if [ "$status_code" -ge 400 ]; then
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Update failed with status code $status_code"
|
||||
echo "Response body: $body"
|
||||
exit 1
|
||||
else
|
||||
echo "Updated succeeded!"
|
||||
fi
|
||||
|
||||
- name: Trigger Deployment
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
# wait 10 secs
|
||||
sleep 10
|
||||
|
||||
response=$(curl -X POST \
|
||||
"${DEPLOY_DOMAIN}/api/application.deploy" \
|
||||
--max-time 30 \
|
||||
-H "accept: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"applicationId\":\"${CLIENT_APPID}\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
echo "Status code: $status_code"
|
||||
|
||||
if [ "$status_code" -ge 400 ]; then
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Deployment failed with status code $status_code"
|
||||
echo "Response body: $body"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment succeeded!"
|
||||
fi
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
# Wait for deployment to complete
|
||||
sleep 30
|
||||
|
||||
if ! curl -s -f "${{ vars.CLIENT_PROD_URL }}/"; then
|
||||
echo "Prod environment is not healthy"
|
||||
exit 1
|
||||
else
|
||||
echo "Prod environment is healthy"
|
||||
fi
|
||||
@@ -1,51 +1,17 @@
|
||||
name: Main
|
||||
name: Main Server
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
paths:
|
||||
- 'server/**'
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '*.yml'
|
||||
|
||||
jobs:
|
||||
client-code-checks:
|
||||
name: Client Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Clean and Install
|
||||
run: cd client && npm ci
|
||||
- name: Build
|
||||
run: cd client && npm run build
|
||||
- name: Lint
|
||||
run: cd client && npm run lint
|
||||
- name: Unit tests
|
||||
run: cd client && npm run test:coverage
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5.0.0
|
||||
with:
|
||||
projectBaseDir: client
|
||||
args: >
|
||||
-Dsonar.organization=ricardo-campos-org
|
||||
-Dsonar.projectKey=ricardo-campos-org_react-typescript-todolist_client
|
||||
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
-Dsonar.typescript.tsconfigPaths=tsconfig.json
|
||||
-Dsonar.sources=src/
|
||||
-Dsonar.exclusions=src/__test__/**
|
||||
-Dsonar.tests=src/__test__/
|
||||
-Dsonar.verbose=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
java-code-checks:
|
||||
name: Server Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
@@ -103,17 +69,6 @@ jobs:
|
||||
GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }}
|
||||
API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
SERVER_APPID: ${{ secrets.PROD_API_CLIENT_ID }}
|
||||
CLIENT_APPID: ${{ secrets.PROD_WEB_CLIENT_ID }}
|
||||
strategy:
|
||||
matrix:
|
||||
name: [server, client]
|
||||
include:
|
||||
- name: server
|
||||
health_check_url: "${{ vars.API_PROD_URL }}/actuator/health"
|
||||
app_id: ${SERVER_APPID}
|
||||
- name: client
|
||||
health_check_url: "${{ vars.CLIENT_PROD_URL }}/"
|
||||
app_id: ${CLIENT_APPID}
|
||||
steps:
|
||||
- uses: docker/login-action@v3
|
||||
name: Login to GitHub Container Registry
|
||||
@@ -123,20 +78,20 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull image with source tag
|
||||
run: docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/${{ matrix.name }}:candidate
|
||||
run: docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate
|
||||
|
||||
- name: Extract PR number using docker inspect
|
||||
id: inspect
|
||||
run: |
|
||||
SOURCE_PR=$(docker inspect ghcr.io/ricardo-campos-org/react-typescript-todolist/${{ matrix.name }}:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p')
|
||||
SOURCE_PR=$(docker inspect ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p')
|
||||
echo "SOURCE_PR=$SOURCE_PR" >> $GITHUB_ENV
|
||||
echo "source_pr=$SOURCE_PR" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Re-tag the image
|
||||
run: docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/${{ matrix.name }}:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/${{ matrix.name }}:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
run: docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/server:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
|
||||
- name: Push new tag
|
||||
run: docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/${{ matrix.name }}:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
run: docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/server:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
|
||||
- name: Update image tag to be deployed
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
@@ -145,16 +100,18 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: |
|
||||
response=$(curl -X POST \
|
||||
"${DEPLOY_DOMAIN}/api/application.saveDockerProvider" \
|
||||
"'${DEPLOY_DOMAIN}/api/application.saveDockerProvider'" \
|
||||
--max-time 30 \
|
||||
-H "accept: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"applicationId\":\"${{ matrix.app_id }}\"" \
|
||||
",\"dockerImage\": \"ghcr.io/ricardo-campos-org/react-typescript-todolist/${{ matrix.name }}:prod-${{ steps.inspect.outputs.source_pr }}\"" \
|
||||
",\"username\": \"${GHCR_USERNAME}\"" \
|
||||
",\"password\": \"${GHCR_PASSWORD}\"" \
|
||||
",\"registryUrl\": \"ghcr.io\"}" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d '{
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/server:prod-${{ steps.inspect.outputs.source_pr }}",
|
||||
"applicationId": "${SERVER_APPID}",
|
||||
"username": "${GHCR_USERNAME}",
|
||||
"password": "${GHCR_PASSWORD}",
|
||||
"registryUrl": "ghcr.io"
|
||||
}' \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
@@ -186,7 +143,7 @@ jobs:
|
||||
-H "accept: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"applicationId\":\"${{ matrix.app_id }}\"}" \
|
||||
-d "{\"applicationId\":\"${SERVER_APPID}\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
@@ -208,7 +165,7 @@ jobs:
|
||||
# Wait for deployment to complete
|
||||
sleep 30
|
||||
|
||||
if ! curl -s -f "${{ matrix.health_check_url }}"; then
|
||||
if ! curl -s -f "${{ vars.API_PROD_URL }}/actuator/health"; then
|
||||
echo "Prod environment is not healthy"
|
||||
exit 1
|
||||
else
|
||||
@@ -0,0 +1,141 @@
|
||||
name: PR Client
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'client/**'
|
||||
|
||||
concurrency:
|
||||
group: client-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
client-code-checks:
|
||||
name: Client Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
name: Set up Node
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Clean and Install
|
||||
run: cd client && npm ci
|
||||
- name: Build
|
||||
run: cd client && npm run build
|
||||
- name: Lint
|
||||
run: cd client && npm run lint
|
||||
- name: Unit tests
|
||||
run: cd client && npm run test:coverage
|
||||
- name: SonarCloud Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5.0.0
|
||||
with:
|
||||
projectBaseDir: client
|
||||
args: >
|
||||
-Dsonar.organization=ricardo-campos-org
|
||||
-Dsonar.projectKey=ricardo-campos-org_react-typescript-todolist_client
|
||||
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
-Dsonar.typescript.tsconfigPaths=tsconfig.json
|
||||
-Dsonar.sources=src/
|
||||
-Dsonar.exclusions=src/__test__/**
|
||||
-Dsonar.tests=src/__test__/
|
||||
-Dsonar.verbose=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
client-docker-build:
|
||||
name: Build Client Docker image
|
||||
runs-on: ubuntu-latest
|
||||
needs: client-code-checks
|
||||
env:
|
||||
VITE_BUILD: client:${{ github.event.number }}
|
||||
VITE_BACKEND_SERVER: ${{ secrets.SERVER_ADDRESS }}/server
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
name: Set up Docker Buildx
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
name: Login to GitHub Container Registry
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d-%H%M%S')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: ./client
|
||||
tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate
|
||||
build-args: |
|
||||
VITE_BUILD=v${{ github.event.number }}-${{ steps.date.outputs.date }}
|
||||
SOURCE_PR=v${{ github.event.number }}-${{ github.run_id }}-${{ steps.date.outputs.date }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
client-stage-deployments:
|
||||
name: Deploy Changes to Stage
|
||||
runs-on: ubuntu-latest
|
||||
needs: client-docker-build
|
||||
if: github.event.pull_request.user.login == 'rmcampos' && github.event_name == 'pull_request'
|
||||
env:
|
||||
DEPLOY_DOMAIN: ${{ vars.DEPLOY_DOMAIN }}
|
||||
API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
CLIENT_APPID: ${{ secrets.STAGE_WEB_CLIENT_ID }}
|
||||
steps:
|
||||
- name: Pre-deployment check
|
||||
run: |
|
||||
if ! curl -s -f "${{ vars.CLIENT_STAGE_URL }}"; then
|
||||
echo "Stage environment is not healthy"
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
|
||||
- name: Trigger Deployment
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
response=$(curl -X POST \
|
||||
"${DEPLOY_DOMAIN}/api/application.deploy" \
|
||||
--max-time 30 \
|
||||
-H "accept: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"applicationId\":\"${CLIENT_APPID}\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
echo "Status code: $status_code"
|
||||
|
||||
if [ "$status_code" -ge 400 ]; then
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Deployment failed with status code $status_code"
|
||||
echo "Response body: $body"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment succeeded!"
|
||||
fi
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
# Wait for deployment to complete
|
||||
sleep 30
|
||||
|
||||
if ! curl -s -f "${{ vars.CLIENT_STAGE_URL }}"; then
|
||||
echo "Stage environment is not healthy"
|
||||
exit 1
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
@@ -1,111 +1,16 @@
|
||||
name: PR
|
||||
name: PR Server
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'client/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event.number }}
|
||||
group: server-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# JOB to run change detection
|
||||
changes:
|
||||
name: Check changes
|
||||
runs-on: ubuntu-latest
|
||||
# Required permissions
|
||||
permissions:
|
||||
pull-requests: read
|
||||
# Set job outputs to values from filter step
|
||||
outputs:
|
||||
server: ${{ steps.filter.outputs.server }}
|
||||
client: ${{ steps.filter.outputs.client }}
|
||||
steps:
|
||||
# For pull requests it's not necessary to checkout the code
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
server:
|
||||
- 'server/**'
|
||||
client:
|
||||
- 'client/**'
|
||||
|
||||
client-code-checks:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.client == 'true' }}
|
||||
name: Client Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
name: Set up Node
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Clean and Install
|
||||
run: cd client && npm ci
|
||||
- name: Build
|
||||
run: cd client && npm run build
|
||||
- name: Lint
|
||||
run: cd client && npm run lint
|
||||
- name: Unit tests
|
||||
run: cd client && npm run test:coverage
|
||||
- name: SonarCloud Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5.0.0
|
||||
with:
|
||||
projectBaseDir: client
|
||||
args: >
|
||||
-Dsonar.organization=ricardo-campos-org
|
||||
-Dsonar.projectKey=ricardo-campos-org_react-typescript-todolist_client
|
||||
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
-Dsonar.typescript.tsconfigPaths=tsconfig.json
|
||||
-Dsonar.sources=src/
|
||||
-Dsonar.exclusions=src/__test__/**
|
||||
-Dsonar.tests=src/__test__/
|
||||
-Dsonar.verbose=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
client-docker-build:
|
||||
name: Build Client Docker image
|
||||
runs-on: ubuntu-latest
|
||||
needs: client-code-checks
|
||||
env:
|
||||
VITE_BUILD: client:${{ github.event.number }}
|
||||
VITE_BACKEND_SERVER: ${{ secrets.SERVER_ADDRESS }}/server
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
name: Set up Docker Buildx
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
name: Login to GitHub Container Registry
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d-%H%M%S')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: ./client
|
||||
tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate
|
||||
build-args: |
|
||||
VITE_BUILD=v${{ github.event.number }}-${{ steps.date.outputs.date }}
|
||||
SOURCE_PR=v${{ github.event.number }}-${{ github.run_id }}-${{ steps.date.outputs.date }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
java-code-checks:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.server == 'true' }}
|
||||
name: Server Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -187,30 +92,20 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
stage-deployments:
|
||||
java-stage-deployments:
|
||||
name: Deploy Changes to Stage
|
||||
runs-on: ubuntu-latest
|
||||
needs: [java-docker-build, client-docker-build]
|
||||
needs: java-docker-build
|
||||
if: github.event.pull_request.user.login == 'rmcampos' && github.event_name == 'pull_request'
|
||||
env:
|
||||
DEPLOY_DOMAIN: ${{ vars.DEPLOY_DOMAIN }}
|
||||
API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
SERVER_APPID: ${{ secrets.STAGE_API_CLIENT_ID }}
|
||||
CLIENT_APPID: ${{ secrets.STAGE_WEB_CLIENT_ID }}
|
||||
strategy:
|
||||
matrix:
|
||||
name: [server, client]
|
||||
include:
|
||||
- name: server
|
||||
health_check_url: "${{ vars.API_STAGE_URL }}/actuator/health"
|
||||
app_id: ${SERVER_APPID}
|
||||
- name: client
|
||||
health_check_url: "${{ vars.CLIENT_STAGE_URL }}/"
|
||||
app_id: ${CLIENT_APPID}
|
||||
steps:
|
||||
- name: Pre-deployment check
|
||||
run: |
|
||||
if ! curl -s -f "${{ matrix.health_check_url }}"; then
|
||||
if ! curl -s -f "${{ vars.API_STAGE_URL }}/actuator/health"; then
|
||||
echo "Stage environment is not healthy"
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
@@ -228,7 +123,7 @@ jobs:
|
||||
-H "accept: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"applicationId\":\"${{ matrix.app_id }}\"}" \
|
||||
-d "{\"applicationId\":\"${SERVER_APPID}\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
@@ -250,9 +145,9 @@ jobs:
|
||||
# Wait for deployment to complete
|
||||
sleep 30
|
||||
|
||||
if ! curl -s -f "${{ matrix.health_check_url }}"; then
|
||||
if ! curl -s -f "${{ vars.API_STAGE_URL }}/actuator/health"; then
|
||||
echo "Stage environment is not healthy"
|
||||
exit 1
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
fi
|
||||
Generated
+159
@@ -20,6 +20,7 @@
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-bootstrap-icons": "^1.11.5",
|
||||
"react-charts": "^3.0.0-beta.57",
|
||||
"react-datepicker": "^8.2.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -1095,6 +1096,59 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.5.tgz",
|
||||
"integrity": "sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.9",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -3386,6 +3440,15 @@
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -3659,6 +3722,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@@ -7735,6 +7808,21 @@
|
||||
"@types/react": "^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-datepicker": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.2.1.tgz",
|
||||
"integrity": "sha512-1pyALWM9mTZ7DG7tfcApwBy2kkld9Kz/EI++LhPnoXJAASbvuq6fdsDfkoB3q1JrxF7vhghVmQ759H/rOwUNNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.3",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
@@ -8793,6 +8881,12 @@
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
@@ -10372,6 +10466,46 @@
|
||||
"levn": "^0.4.1"
|
||||
}
|
||||
},
|
||||
"@floating-ui/core": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"requires": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"@floating-ui/dom": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"requires": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"@floating-ui/react": {
|
||||
"version": "0.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.5.tgz",
|
||||
"integrity": "sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ==",
|
||||
"requires": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.9",
|
||||
"tabbable": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"requires": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
|
||||
},
|
||||
"@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -11791,6 +11925,11 @@
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
|
||||
},
|
||||
"clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -12002,6 +12141,11 @@
|
||||
"is-data-view": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@@ -14748,6 +14892,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-datepicker": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.2.1.tgz",
|
||||
"integrity": "sha512-1pyALWM9mTZ7DG7tfcApwBy2kkld9Kz/EI++LhPnoXJAASbvuq6fdsDfkoB3q1JrxF7vhghVmQ759H/rOwUNNw==",
|
||||
"requires": {
|
||||
"@floating-ui/react": "^0.27.3",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
@@ -15466,6 +15620,11 @@
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
||||
},
|
||||
"tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-bootstrap-icons": "^1.11.5",
|
||||
"react-charts": "^3.0.0-beta.57",
|
||||
"react-datepicker": "^8.2.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { test, vi } from 'vitest';
|
||||
import App from '../App';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import AuthContext from '../context/AuthContext';
|
||||
import authContextMock from './__mocks__/authContextMock';
|
||||
import SidebarContext from '../context/SidebarContext';
|
||||
@@ -11,12 +11,18 @@ const sidebarContextMock = {
|
||||
setNewPage: vi.fn()
|
||||
};
|
||||
|
||||
test('Renders the app', () => {
|
||||
render(
|
||||
<AuthContext.Provider value={authContextMock}>
|
||||
<SidebarContext.Provider value={sidebarContextMock}>
|
||||
<App />
|
||||
</SidebarContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
vi.mock('react-charts', () => ({
|
||||
Chart: ({ options }) => <div data-testid="mocked-chart">Mocked Chart</div>
|
||||
}));
|
||||
|
||||
test('Renders the app', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<AuthContext.Provider value={authContextMock}>
|
||||
<SidebarContext.Provider value={sidebarContextMock}>
|
||||
<App />
|
||||
</SidebarContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,23 @@ import { describe, expect, it } from 'vitest';
|
||||
import TaskTimeLeft from '../../components/TaskTimeLeft';
|
||||
|
||||
describe('TaskTimeLeft Component', () => {
|
||||
const renderComponent = (done: boolean) => {
|
||||
return render(
|
||||
<TaskTimeLeft
|
||||
text="2 days left"
|
||||
done={done}
|
||||
tooltip="2025-03-20"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('should render the TaskTimeLeft component with text when task is not done', () => {
|
||||
const { getByText } = render(<TaskTimeLeft text="2 days left" done={false} />);
|
||||
const { getByText } = renderComponent(false);
|
||||
expect(getByText('2 days left')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not render the TaskTimeLeft component when task is done', () => {
|
||||
const { queryByText } = render(<TaskTimeLeft text="2 days left" done={true} />);
|
||||
const { queryByText } = renderComponent(true);
|
||||
expect(queryByText('2 days left')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,20 +146,26 @@ describe('AuthProvider', () => {
|
||||
|
||||
vi.spyOn(api, 'putJSON').mockResolvedValue(fakeResponse);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<AuthProvider>
|
||||
<ConsumerComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
|
||||
let getByTestIdFunction;
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('register'));
|
||||
const { getByTestId } = render(
|
||||
<AuthProvider>
|
||||
<ConsumerComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
getByTestIdFunction = getByTestId;
|
||||
});
|
||||
|
||||
await waitFor(() => expect(getByTestIdFunction('register')).toBeDefined());
|
||||
|
||||
await user.click(getByTestIdFunction('register'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('signed').textContent).toBe('true')
|
||||
expect(getByTestIdFunction('signed').textContent).toBe('true')
|
||||
);
|
||||
expect(getByTestId('user').textContent).toBe('New User');
|
||||
expect(getByTestIdFunction('user').textContent).toBe('New User');
|
||||
expect(localStorage.getItem(API_TOKEN)).toBe('register-token');
|
||||
expect(localStorage.getItem(USER_DATA)).not.toBeNull();
|
||||
});
|
||||
@@ -261,16 +267,23 @@ describe('AuthProvider', () => {
|
||||
// Store API_TOKEN so that fetchCurrentSession runs the refresh logic.
|
||||
localStorage.setItem(API_TOKEN, 'dummy');
|
||||
|
||||
const { getByTestId } = render(
|
||||
<AuthProvider>
|
||||
<ConsumerComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByTestId('checkCurrentAuthUser'));
|
||||
let getByTestIdFunction;
|
||||
await act(async () => {
|
||||
const { getByTestId } = render(
|
||||
<AuthProvider>
|
||||
<ConsumerComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
getByTestIdFunction = getByTestId;
|
||||
});
|
||||
|
||||
// Wait for any initial renders to complete
|
||||
await waitFor(() => expect(getByTestIdFunction('checkCurrentAuthUser')).toBeDefined());
|
||||
|
||||
await user.click(getByTestIdFunction('checkCurrentAuthUser'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(localStorage.getItem(API_TOKEN)).toBe('refresh-token')
|
||||
);
|
||||
|
||||
@@ -1,20 +1,136 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { TaskResponse } from '../../types/TaskResponse';
|
||||
import api from '../../api-service/api';
|
||||
import Task from '../../views/Task';
|
||||
import '../../i18n';
|
||||
|
||||
vi.mock('../../api-service/api');
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
i18n: {
|
||||
changeLanguage: vi.fn(),
|
||||
language: 'en',
|
||||
},
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: '3rdParty',
|
||||
init: vi.fn(),
|
||||
},
|
||||
I18nextProvider: ({ children }: any) => children,
|
||||
}));
|
||||
|
||||
const mockTask: TaskResponse[] = [
|
||||
{
|
||||
id: 1,
|
||||
description: 'Task_one',
|
||||
done: false,
|
||||
highPriority: false,
|
||||
dueDate: '',
|
||||
dueDateFmt: '',
|
||||
lastUpdate: '1 day ago',
|
||||
tag: 'test1',
|
||||
urls: ['http://test.copm']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
description: 'Task_two',
|
||||
done: false,
|
||||
highPriority: true,
|
||||
dueDate: '2025-12-31',
|
||||
dueDateFmt: '7 months left',
|
||||
lastUpdate: '2 days ago',
|
||||
tag: 'test2',
|
||||
urls: ['http://test.copm']
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
description: 'Task_three',
|
||||
done: true,
|
||||
highPriority: true,
|
||||
dueDate: '',
|
||||
dueDateFmt: '',
|
||||
lastUpdate: '3 days ago',
|
||||
tag: 'test3',
|
||||
urls: ['http://test.copm']
|
||||
}
|
||||
];
|
||||
|
||||
describe('Renders the task view', () => {
|
||||
it('should render the task view', async() => {
|
||||
render(
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderTask = () => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<Task />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('should render the Task view and load tasks', async () => {
|
||||
vi.spyOn(api, 'getJSON').mockResolvedValue(mockTask);
|
||||
|
||||
renderTask();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.main-margin')).toBeDefined();
|
||||
// normal priority task
|
||||
expect(screen.getByText('Task_one')).toBeDefined();
|
||||
expect(screen.queryByTestId('task-title-bell-Task_one')).toBeNull();
|
||||
expect(Array.from(screen.getByTestId('task-title-text-Task_one').classList).join(' ')).toBe('poppins-semibold');
|
||||
|
||||
// high priority task
|
||||
expect(screen.getByText('Task_two')).toBeDefined();
|
||||
expect(screen.getByTestId('task-title-bell-Task_two')).toBeDefined();
|
||||
expect(Array.from(screen.getByTestId('task-title-text-Task_two').classList).join(' ')).toBe('ms-2 poppins-semibold');
|
||||
|
||||
// done task
|
||||
expect(screen.getByText('Task_three')).toBeDefined();
|
||||
expect(screen.getByTestId('task-title-check-Task_three')).toBeDefined();
|
||||
expect(Array.from(screen.getByTestId('task-title-text-Task_three').classList).join(' ')).toBe('text-strike ms-2 poppins-semibold');
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter notes based on input text', async () => {
|
||||
vi.spyOn(api, 'getJSON').mockResolvedValue(mockTask);
|
||||
|
||||
renderTask();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Task_one')).toBeDefined();
|
||||
expect(screen.getByText('Task_two')).toBeDefined();
|
||||
expect(screen.getByText('Task_three')).toBeDefined();
|
||||
});
|
||||
|
||||
// filter by text 'one'
|
||||
fireEvent.change(screen.getByPlaceholderText('Filter tasks'), { target: { value: 'one' } });
|
||||
|
||||
expect(screen.getByText('Task_one')).toBeDefined();
|
||||
expect(screen.queryByText('Task_two')).toBeNull();
|
||||
expect(screen.queryByText('Task_three')).toBeNull();
|
||||
});
|
||||
|
||||
it('should filter notes based on radio change', async () => {
|
||||
vi.spyOn(api, 'getJSON').mockResolvedValue(mockTask);
|
||||
|
||||
renderTask();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Task_one')).toBeDefined();
|
||||
expect(screen.getByText('Task_two')).toBeDefined();
|
||||
expect(screen.getByText('Task_three')).toBeDefined();
|
||||
});
|
||||
|
||||
// filter only completed
|
||||
fireEvent.click(screen.getByLabelText('Completed'));
|
||||
|
||||
expect(screen.queryByText('Task_one')).toBeNull();
|
||||
expect(screen.queryByText('Task_two')).toBeNull();
|
||||
expect(screen.getByText('Task_three')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/* custom-datepicker.css */
|
||||
.react-datepicker__input-container input {
|
||||
font-size: 16px; /* Prevents iOS zoom on focus */
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day {
|
||||
margin: 0.2rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
/* Larger touch targets on mobile */
|
||||
@media (max-width: 768px) {
|
||||
.react-datepicker__day,
|
||||
.react-datepicker__month-text,
|
||||
.react-datepicker__quarter-text,
|
||||
.react-datepicker__year-text {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Col, Form, InputGroup, Row } from 'react-bootstrap';
|
||||
import * as Icons from 'react-bootstrap-icons';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import './custom-datepicker.css';
|
||||
import { MiddlewareReturn } from '@floating-ui/core';
|
||||
import { MiddlewareState } from '@floating-ui/dom';
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
|
||||
@@ -11,8 +16,10 @@ interface Props {
|
||||
type?: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
value?: string;
|
||||
valueDate?: Date | null;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onChangeDate?: (date: Date | null) => void;
|
||||
data_testid?: string;
|
||||
}
|
||||
|
||||
@@ -74,15 +81,49 @@ function FormInput(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
<InputGroup.Text>
|
||||
<Icon />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
required={props.required}
|
||||
type={formType}
|
||||
name={props.name}
|
||||
placeholder={props.placeholder ? props.placeholder : ''}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
data-testid={props.data_testid}
|
||||
/>
|
||||
{props.type == 'date'
|
||||
? (
|
||||
<DatePicker
|
||||
selected={props?.valueDate}
|
||||
onChange={(date: Date | null) => {
|
||||
if (props.onChangeDate) {
|
||||
props.onChangeDate(date);
|
||||
}
|
||||
}}
|
||||
dateFormat="MMMM d, yyyy"
|
||||
className="form-control"
|
||||
id="date-input"
|
||||
popperPlacement="bottom"
|
||||
popperModifiers={[
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
},
|
||||
fn: function (state: MiddlewareState): MiddlewareReturn | Promise<MiddlewareReturn> {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
]}
|
||||
// Mobile-friendly options
|
||||
withPortal
|
||||
showYearDropdown
|
||||
showMonthDropdown
|
||||
dropdownMode="select"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Form.Control
|
||||
required={props.required}
|
||||
type={formType}
|
||||
name={props.name}
|
||||
placeholder={props.placeholder ? props.placeholder : ''}
|
||||
value={props?.value}
|
||||
onChange={props.onChange}
|
||||
data-testid={props.data_testid}
|
||||
/>
|
||||
)}
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
</Col>
|
||||
|
||||
@@ -1,28 +1,56 @@
|
||||
import React from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { CalendarCheck } from 'react-bootstrap-icons';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
done: boolean;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the TaskTimeLeft component if the task is not done, displaying
|
||||
* a calendar icon and the time left for the task.
|
||||
*
|
||||
* @param {Props.done} props.done - Boolean value indicating if the task is done.
|
||||
* @param {Props.text} props.text - The time left for the task.
|
||||
* @param {Props} props - Props for the TaskTimeLeft component.
|
||||
* @param {string} props.text - The time left for the task.
|
||||
* @param {boolean} props.done - Boolean value indicating if the task is done.
|
||||
* @param {string} props.tooltip - The string representation of a Date instance.
|
||||
* @returns
|
||||
*/
|
||||
function TaskTimeLeft(props: Props): React.ReactNode | null {
|
||||
function TaskTimeLeft(props: React.PropsWithChildren<Props>): React.ReactNode | null {
|
||||
const tooltipDate = props.tooltip.length > 0
|
||||
? new Intl.DateTimeFormat('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
}).format(new Date(props.tooltip))
|
||||
: '';
|
||||
|
||||
return props.done
|
||||
? null
|
||||
: (
|
||||
<div className="d-block">
|
||||
<CalendarCheck />
|
||||
<span className="ms-2 poppins-regular">
|
||||
{props.text}
|
||||
</span>
|
||||
{props.tooltip.length > 0 && (
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={(
|
||||
<Tooltip id="tooltip-right">
|
||||
{tooltipDate}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<span className="ms-2 poppins-regular">
|
||||
{props.text}
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{props.tooltip.length === 0 && (
|
||||
<span className="ms-2 poppins-regular">
|
||||
{props.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,26 +20,32 @@ interface Props {
|
||||
function TaskTitle(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
if (props.highPriority) {
|
||||
return (
|
||||
<span className="task-title-icon">
|
||||
<span className="task-title-icon" data-testid={`task-title-container-${props.title}`}>
|
||||
{props.done
|
||||
? (
|
||||
<Check2Circle />
|
||||
<Check2Circle data-testid={`task-title-check-${props.title}`} />
|
||||
)
|
||||
: (
|
||||
<Bell />
|
||||
<Bell data-testid={`task-title-bell-${props.title}`} />
|
||||
)}
|
||||
<span className={`${props.done ? 'text-strike' : ''} ms-2 poppins-semibold`}>
|
||||
<span
|
||||
className={`${props.done ? 'text-strike' : ''} ms-2 poppins-semibold`}
|
||||
data-testid={`task-title-text-${props.title}`}
|
||||
>
|
||||
{props.title}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="task-title-icon">
|
||||
<span className="task-title-icon" data-testid={`task-title-container-${props.title}`}>
|
||||
{props.done && (
|
||||
<Check2Circle />
|
||||
<Check2Circle data-testid={`task-title-check-${props.title}`} />
|
||||
)}
|
||||
<span className={`${props.done ? 'ms-2 text-strike' : ''} poppins-semibold`}>
|
||||
<span
|
||||
className={`${props.done ? 'ms-2 text-strike' : ''} poppins-semibold`}
|
||||
data-testid={`task-title-text-${props.title}`}
|
||||
>
|
||||
{props.title}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -19,9 +19,9 @@ import TaskTimeLeft from '../../components/TaskTimeLeft';
|
||||
import TaskUrl from '../../components/TaskUrl';
|
||||
import TaskTag from '../../components/TaskTag';
|
||||
import TaskTitle from '../../components/TaskTitle';
|
||||
import './style.css';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
import AlertError from '../../components/AlertError';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
* The Task component is a view that displays a list of tasks.
|
||||
@@ -33,6 +33,7 @@ function Task(): React.ReactNode {
|
||||
const [tasks, setTasks] = useState<TaskResponse[]>([]);
|
||||
const [savedTasks, setSavedTasks] = useState<TaskResponse[]>([]);
|
||||
const [filterText, setFilterText] = useState<string>('');
|
||||
const [selectedOption, setSelectedOption] = useState<string>('option1');
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
/**
|
||||
@@ -64,6 +65,7 @@ function Task(): React.ReactNode {
|
||||
const translated = translateTaskResponse(tasksFetched, i18n.language);
|
||||
setSavedTasks([...translated]);
|
||||
setTasks([...translated]);
|
||||
setSelectedOption('option1');
|
||||
}
|
||||
catch (e) {
|
||||
handleError(e);
|
||||
@@ -107,24 +109,38 @@ function Task(): React.ReactNode {
|
||||
/**
|
||||
* Filter tasks by a given text.
|
||||
*/
|
||||
const filterTasks = (text: string): void => {
|
||||
const filterTasks = (text: string, radioBtnFilter?: string): void => {
|
||||
setFilterText(text);
|
||||
|
||||
if (!text) {
|
||||
if (!text && !radioBtnFilter) {
|
||||
setTasks([...savedTasks]);
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredTasks = savedTasks.filter((task: TaskResponse) => {
|
||||
let filteredTasks = savedTasks.filter((task: TaskResponse) => {
|
||||
const shouldFilter = task.description.toLowerCase().includes(text.toLowerCase())
|
||||
|| task.tag.toLowerCase().includes(text.toLowerCase())
|
||||
|| task.urls.filter((url: string) => url.includes(text.toLowerCase())).length > 0;
|
||||
return shouldFilter;
|
||||
});
|
||||
|
||||
const pending = radioBtnFilter === 'option2';
|
||||
const completed = radioBtnFilter === 'option3';
|
||||
if (pending) {
|
||||
filteredTasks = filteredTasks.filter((task: TaskResponse) => !task.done);
|
||||
}
|
||||
else if (completed) {
|
||||
filteredTasks = filteredTasks.filter((task: TaskResponse) => task.done);
|
||||
}
|
||||
|
||||
setTasks([...filteredTasks]);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setSelectedOption(e.target.value);
|
||||
filterTasks(filterText, e.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, []);
|
||||
@@ -150,7 +166,7 @@ function Task(): React.ReactNode {
|
||||
name="search_term"
|
||||
placeholder="Filter tasks"
|
||||
value={filterText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => filterTasks(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => filterTasks(e.target.value, selectedOption)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={3}>
|
||||
@@ -164,6 +180,48 @@ function Task(): React.ReactNode {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<Form className="mt-3 ms-1">
|
||||
<div className="d-flex gap-3">
|
||||
<Form.Check
|
||||
inline
|
||||
type="radio"
|
||||
label="All tasks"
|
||||
name="radioGroup"
|
||||
id="radio1"
|
||||
value="option1"
|
||||
checked={selectedOption === 'option1'}
|
||||
onChange={handleChange}
|
||||
className="custom-radio-button"
|
||||
/>
|
||||
<Form.Check
|
||||
inline
|
||||
type="radio"
|
||||
label="Pending"
|
||||
name="radioGroup"
|
||||
id="radio2"
|
||||
value="option2"
|
||||
checked={selectedOption === 'option2'}
|
||||
onChange={handleChange}
|
||||
className="custom-radio-button"
|
||||
/>
|
||||
<Form.Check
|
||||
inline
|
||||
type="radio"
|
||||
label="Completed"
|
||||
name="radioGroup"
|
||||
id="radio3"
|
||||
value="option3"
|
||||
checked={selectedOption === 'option3'}
|
||||
onChange={handleChange}
|
||||
className="custom-radio-button"
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="mt-3">
|
||||
{tasks.map((task: TaskResponse) => (
|
||||
<Col xs={12} key={task.id.toString()}>
|
||||
@@ -204,7 +262,11 @@ function Task(): React.ReactNode {
|
||||
</Row>
|
||||
|
||||
{task.dueDateFmt && (
|
||||
<TaskTimeLeft text={task.dueDateFmt} done={task.done} />
|
||||
<TaskTimeLeft
|
||||
text={task.dueDateFmt}
|
||||
done={task.done}
|
||||
tooltip={task.dueDate}
|
||||
/>
|
||||
)}
|
||||
{task.urls.length > 0 && (
|
||||
<TaskUrl url={task.urls[0]} />
|
||||
|
||||
@@ -67,4 +67,79 @@
|
||||
.task-card-footer {
|
||||
background-color: #fff !important;
|
||||
border-top: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Radio Buttons */
|
||||
/* Container styling */
|
||||
.radio-group-container {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Label styling */
|
||||
.radio-group-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Radio buttons wrapper */
|
||||
.radio-buttons-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Custom radio button styling */
|
||||
.custom-radio-button .form-check-input {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
margin-top: 0.2rem;
|
||||
cursor: pointer;
|
||||
border: 2px solid #6c757d;
|
||||
}
|
||||
|
||||
.custom-radio-button .form-check-input:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.custom-radio-button .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.custom-radio-button .form-check-label {
|
||||
font-size: 1rem;
|
||||
padding-left: 0.3rem;
|
||||
cursor: pointer;
|
||||
color: #495057;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.custom-radio-button:hover .form-check-label {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Selected option text */
|
||||
.selected-option {
|
||||
font-weight: 500;
|
||||
color: #0d6efd;
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem;
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 576px) {
|
||||
.radio-buttons-wrapper {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ function TaskAdd(): React.ReactNode {
|
||||
const [taskUrl, setTaskUrl] = useState<string>('');
|
||||
const [taskDone, setTaskDone] = useState<boolean>(false);
|
||||
const [action, setAction] = useState<TaskAction>('add');
|
||||
const [dueDate, setDueDate] = useState<string>('');
|
||||
const [dueDate, setDueDate] = useState<Date | null>(null);
|
||||
const [highPriority, setHighPriority] = useState<boolean>(false);
|
||||
const [tag, setTag] = useState<string>('');
|
||||
const { i18n, t } = useTranslation();
|
||||
@@ -97,7 +97,7 @@ function TaskAdd(): React.ReactNode {
|
||||
setTaskDescription('');
|
||||
setTaskDone(false);
|
||||
setTaskUrl('');
|
||||
setDueDate('');
|
||||
setDueDate(null);
|
||||
setHighPriority(false);
|
||||
setTag('');
|
||||
|
||||
@@ -121,11 +121,16 @@ function TaskAdd(): React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
let dueDateFormatted: string = '';
|
||||
if (dueDate) {
|
||||
dueDateFormatted = dueDate.toISOString().substring(0, 10);
|
||||
}
|
||||
|
||||
if (action === 'add') {
|
||||
const addPayload: TaskNoteRequest = {
|
||||
description: taskDescription.trim(),
|
||||
highPriority: highPriority,
|
||||
dueDate: dueDate || '',
|
||||
dueDate: dueDateFormatted,
|
||||
tag: tag,
|
||||
urls: taskUrl ? [taskUrl] : []
|
||||
};
|
||||
@@ -143,7 +148,7 @@ function TaskAdd(): React.ReactNode {
|
||||
description: taskDescription.trim(),
|
||||
done: taskDone,
|
||||
highPriority: highPriority,
|
||||
dueDate: dueDate || '',
|
||||
dueDate: dueDateFormatted,
|
||||
dueDateFmt: '',
|
||||
lastUpdate: '',
|
||||
tag: tag,
|
||||
@@ -171,7 +176,7 @@ function TaskAdd(): React.ReactNode {
|
||||
setTaskUrl(taskToEdit.urls.length ? taskToEdit.urls[0] : '');
|
||||
setTaskDone(taskToEdit.done);
|
||||
if (taskToEdit.dueDateFmt) {
|
||||
setDueDate(taskToEdit.dueDate);
|
||||
setDueDate(new Date(taskToEdit.dueDate));
|
||||
}
|
||||
setHighPriority(taskToEdit.highPriority);
|
||||
if (taskToEdit.tag) {
|
||||
@@ -247,12 +252,12 @@ function TaskAdd(): React.ReactNode {
|
||||
labelText={t('task_form_duedate_label')}
|
||||
iconName="CalendarCheck"
|
||||
required={false}
|
||||
type="text"
|
||||
type="date"
|
||||
name="dueDate"
|
||||
placeholder={t('task_form_duedate_placeholder')}
|
||||
value={dueDate}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDueDate(e.target.value);
|
||||
valueDate={dueDate}
|
||||
onChangeDate={(date: Date | null) => {
|
||||
setDueDate(date);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user