diff --git a/.cspell.json b/.cspell.json index c4ca3cbace9..08613228f55 100644 --- a/.cspell.json +++ b/.cspell.json @@ -320,6 +320,9 @@ "signin", "ibase", "iwindow", + "Probot", + "Smee", + "badal", "MAINSTRSERVER", "MAINLOADURL", "MAINDB", @@ -369,7 +372,8 @@ "USERTRX", "MAINUNEXCEPTION", "Harbir", - "Chahal" + "Chahal", + "pino" ], "useGitignore": true, "ignorePaths": [ diff --git a/.deploy/api/Dockerfile b/.deploy/api/Dockerfile index 6c2d943a488..fac9063068c 100644 --- a/.deploy/api/Dockerfile +++ b/.deploy/api/Dockerfile @@ -47,6 +47,9 @@ ARG FACEBOOK_GRAPH_VERSION ARG FACEBOOK_CALLBACK_URL ARG INTEGRATED_USER_DEFAULT_PASS ARG UPWORK_REDIRECT_URL +ARG HUBSTAFF_CLIENT_ID +ARG HUBSTAFF_CLIENT_SECRET +ARG HUBSTAFF_PERSONAL_ACCESS_TOKEN ARG FILE_PROVIDER ARG GAUZY_AI_GRAPHQL_ENDPOINT ARG GAUZY_AI_REST_ENDPOINT @@ -104,6 +107,8 @@ COPY --chown=node:node packages/plugin/package.json ./packages/plugin/ COPY --chown=node:node packages/plugins/integration-ai/package.json ./packages/plugins/integration-ai/ COPY --chown=node:node packages/plugins/integration-hubstaff/package.json ./packages/plugins/integration-hubstaff/ COPY --chown=node:node packages/plugins/integration-upwork/package.json ./packages/plugins/integration-upwork/ +COPY --chown=node:node packages/plugins/integration-github/package.json ./packages/plugins/integration-github/ +COPY --chown=node:node packages/plugins/integration-jira/package.json ./packages/plugins/integration-jira/ COPY --chown=node:node packages/plugins/product-reviews/package.json ./packages/plugins/product-reviews/ COPY --chown=node:node packages/plugins/knowledge-base/package.json ./packages/plugins/knowledge-base/ COPY --chown=node:node packages/plugins/changelog/package.json ./packages/plugins/changelog/ @@ -147,6 +152,8 @@ COPY --chown=node:node packages/plugin/package.json ./packages/plugin/ COPY --chown=node:node packages/plugins/integration-ai/package.json ./packages/plugins/integration-ai/ COPY --chown=node:node packages/plugins/integration-hubstaff/package.json ./packages/plugins/integration-hubstaff/ COPY --chown=node:node packages/plugins/integration-upwork/package.json ./packages/plugins/integration-upwork/ +COPY --chown=node:node packages/plugins/integration-github/package.json ./packages/plugins/integration-github/ +COPY --chown=node:node packages/plugins/integration-jira/package.json ./packages/plugins/integration-jira/ COPY --chown=node:node packages/plugins/product-reviews/package.json ./packages/plugins/product-reviews/ COPY --chown=node:node packages/plugins/knowledge-base/package.json ./packages/plugins/knowledge-base/ COPY --chown=node:node packages/plugins/changelog/package.json ./packages/plugins/changelog/ @@ -185,6 +192,9 @@ ENV DEMO=${DEMO:-false} ENV IS_DOCKER=true +# Temporary disable caching in NX Cloud for builds +ENV NX_NO_CLOUD=true + RUN yarn build:package:api RUN yarn build:api:prod:docker @@ -263,6 +273,9 @@ ENV FACEBOOK_GRAPH_VERSION=${FACEBOOK_GRAPH_VERSION} ENV FACEBOOK_CALLBACK_URL=${FACEBOOK_CALLBACK_URL} ENV INTEGRATED_USER_DEFAULT_PASS=${INTEGRATED_USER_DEFAULT_PASS} ENV UPWORK_REDIRECT_URL=${UPWORK_REDIRECT_URL} +ENV HUBSTAFF_CLIENT_ID=${HUBSTAFF_CLIENT_ID} +ENV HUBSTAFF_CLIENT_SECRET=${HUBSTAFF_CLIENT_SECRET} +ENV HUBSTAFF_PERSONAL_ACCESS_TOKEN=${HUBSTAFF_PERSONAL_ACCESS_TOKEN} ENV FILE_PROVIDER=${FILE_PROVIDER} ENV GAUZY_AI_GRAPHQL_ENDPOINT=${GAUZY_AI_GRAPHQL_ENDPOINT} ENV GAUZY_AI_REST_ENDPOINT=${GAUZY_AI_REST_ENDPOINT} diff --git a/.deploy/webapp/Dockerfile b/.deploy/webapp/Dockerfile index 637a6f5718d..f050805535b 100644 --- a/.deploy/webapp/Dockerfile +++ b/.deploy/webapp/Dockerfile @@ -55,6 +55,8 @@ COPY --chown=node:node packages/plugin/package.json ./packages/plugin/ COPY --chown=node:node packages/plugins/integration-ai/package.json ./packages/plugins/integration-ai/ COPY --chown=node:node packages/plugins/integration-hubstaff/package.json ./packages/plugins/integration-hubstaff/ COPY --chown=node:node packages/plugins/integration-upwork/package.json ./packages/plugins/integration-upwork/ +COPY --chown=node:node packages/plugins/integration-github/package.json ./packages/plugins/integration-github/ +COPY --chown=node:node packages/plugins/integration-jira/package.json ./packages/plugins/integration-jira/ COPY --chown=node:node packages/plugins/product-reviews/package.json ./packages/plugins/product-reviews/ COPY --chown=node:node packages/plugins/knowledge-base/package.json ./packages/plugins/knowledge-base/ COPY --chown=node:node packages/plugins/changelog/package.json ./packages/plugins/changelog/ @@ -90,6 +92,9 @@ ENV NODE_ENV=${NODE_ENV:-production} ENV IS_DOCKER=true +# Temporary disable caching in NX Cloud for builds +ENV NX_NO_CLOUD=true + RUN yarn build:package:gauzy RUN yarn build:gauzy:prod:docker diff --git a/.env.compose b/.env.compose index 45c896a3829..82e3b28711e 100644 --- a/.env.compose +++ b/.env.compose @@ -75,10 +75,25 @@ FACEBOOK_CLIENT_SECRET=XXXXXXX FACEBOOK_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback FACEBOOK_GRAPH_VERSION=v3.0 +# Github OAuth Integration GITHUB_CLIENT_ID=XXXXXXX GITHUB_CLIENT_SECRET=XXXXXXX GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback +# Github App Install Integration +GAUZY_GITHUB_APP_NAME= +GAUZY_GITHUB_APP_ID=XXXXXXX +GAUZY_GITHUB_APP_PRIVATE_KEY= + +# Github Webhook Configuration +GAUZY_GITHUB_WEBHOOK_URL=http://localhost:3000/api/auth/github/webhook +GAUZY_GITHUB_WEBHOOK_SECRET=XXXXXXX + +# Github Redirect URL +GAUZY_GITHUB_REDIRECT_URL=http://localhost:3000/api/integration/github/callback +GAUZY_GITHUB_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/github/setup/installation" +GAUZY_GITHUB_API_VERSION="2022-11-28" + LINKEDIN_CLIENT_ID=XXXXXXX LINKEDIN_CLIENT_SECRET=XXXXXXX LINKEDIN_CALLBACK_URL=http://localhost:3000/api/auth/linked/callback @@ -103,7 +118,18 @@ KEYCLOAK_AUTH_SERVER_URL=XXXXXXX KEYCLOAK_COOKIE_KEY=XXXXXXX INTEGRATED_HUBSTAFF_USER_PASS=hubstaffPassword -UPWORK_REDIRECT_URL=http://localhost:3000/api/integrations/upwork + +# Upwork Integration Config +UPWORK_API_KEY=XXXXXXX +UPWORK_API_SECRET=XXXXXXX +UPWORK_REDIRECT_URL="http://localhost:3000/api/integrations/upwork/callback" +UPWORK_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/upwork" + +# Hubstaff Integration Configuration +HUBSTAFF_CLIENT_ID=XXXXXXX +HUBSTAFF_CLIENT_SECRET=XXXXXXX +HUBSTAFF_REDIRECT_URL="http://localhost:3000/api/integration/hubstaff/callback" +HUBSTAFF_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/hubstaff" # File System: LOCAL | S3 | WASABI | CLOUDINARY FILE_PROVIDER=LOCAL @@ -270,3 +296,15 @@ FEATURE_ROLES_PERMISSION=true # Email Verification FEATURE_EMAIL_VERIFICATION=false + +# GitHub App Integration +GITHUB_INTEGRATION_APP_ID= +GITHUB_INTEGRATION_CLIENT_ID= +GITHUB_INTEGRATION_CLIENT_SECRET= +GITHUB_INTEGRATION_PRIVATE_KEY= +GITHUB_INTEGRATION_WEBHOOK_SECRET= + +# HubStaff Integration +HUBSTAFF_CLIENT_ID= +HUBSTAFF_CLIENT_SECRET= +HUBSTAFF_PERSONAL_ACCESS_TOKEN= diff --git a/.env.docker b/.env.docker index d1fc94ed882..b5c51d679d9 100644 --- a/.env.docker +++ b/.env.docker @@ -76,11 +76,39 @@ FACEBOOK_CLIENT_SECRET= FACEBOOK_GRAPH_VERSION=v3.0 FACEBOOK_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback +# Github OAuth Integration +GITHUB_CLIENT_ID=XXXXXXX +GITHUB_CLIENT_SECRET=XXXXXXX +GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback + +# Github App Install Integration +GAUZY_GITHUB_APP_NAME= +GAUZY_GITHUB_APP_ID=XXXXXXX +GAUZY_GITHUB_APP_PRIVATE_KEY= + +# Github Webhook Configuration +GAUZY_GITHUB_WEBHOOK_URL=http://localhost:3000/api/auth/github/webhook +GAUZY_GITHUB_WEBHOOK_SECRET=XXXXXXX + +# Github Redirect URL +GAUZY_GITHUB_REDIRECT_URL=http://localhost:3000/api/integration/github/callback +GAUZY_GITHUB_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/github/setup/installation" +GAUZY_GITHUB_API_VERSION="2022-11-28" + # Third Party Integration Config INTEGRATED_USER_DEFAULT_PASS= # Upwork Integration Config -UPWORK_REDIRECT_URL=http://localhost:3000/api/integrations/upwork/callback +UPWORK_API_KEY=XXXXXXX +UPWORK_API_SECRET=XXXXXXX +UPWORK_REDIRECT_URL="http://localhost:3000/api/integrations/upwork/callback" +UPWORK_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/upwork" + +# Hubstaff Integration Configuration +HUBSTAFF_CLIENT_ID=XXXXXXX +HUBSTAFF_CLIENT_SECRET=XXXXXXX +HUBSTAFF_REDIRECT_URL="http://localhost:3000/api/integration/hubstaff/callback" +HUBSTAFF_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/hubstaff" # File System: LOCAL | S3 | WASABI | CLOUDINARY FILE_PROVIDER=LOCAL @@ -241,3 +269,15 @@ FEATURE_ROLES_PERMISSION=true # Email Verification FEATURE_EMAIL_VERIFICATION=false + +# GitHub App Integration +GITHUB_INTEGRATION_APP_ID= +GITHUB_INTEGRATION_CLIENT_ID= +GITHUB_INTEGRATION_CLIENT_SECRET= +GITHUB_INTEGRATION_PRIVATE_KEY= +GITHUB_INTEGRATION_WEBHOOK_SECRET= + +# HubStaff Integration +HUBSTAFF_CLIENT_ID= +HUBSTAFF_CLIENT_SECRET= +HUBSTAFF_PERSONAL_ACCESS_TOKEN= diff --git a/.env.local b/.env.local index eaadee80415..dffb5862a64 100644 --- a/.env.local +++ b/.env.local @@ -76,11 +76,39 @@ FACEBOOK_CLIENT_SECRET= FACEBOOK_GRAPH_VERSION=v3.0 FACEBOOK_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback +# Github OAuth Integration +GITHUB_CLIENT_ID=XXXXXXX +GITHUB_CLIENT_SECRET=XXXXXXX +GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback + +# Github App Install Integration +GAUZY_GITHUB_APP_NAME= +GAUZY_GITHUB_APP_ID=XXXXXXX +GAUZY_GITHUB_APP_PRIVATE_KEY= + +# Github Webhook Configuration +GAUZY_GITHUB_WEBHOOK_URL=http://localhost:3000/api/auth/github/webhook +GAUZY_GITHUB_WEBHOOK_SECRET=XXXXXXX + +# Github Redirect URL +GAUZY_GITHUB_REDIRECT_URL=http://localhost:3000/api/integration/github/callback +GAUZY_GITHUB_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/github/setup/installation" +GAUZY_GITHUB_API_VERSION="2022-11-28" + # Third Party Integration Config INTEGRATED_USER_DEFAULT_PASS= # Upwork Integration Config -UPWORK_REDIRECT_URL=http://localhost:3000/api/integrations/upwork/callback +UPWORK_API_KEY=XXXXXXX +UPWORK_API_SECRET=XXXXXXX +UPWORK_REDIRECT_URL="http://localhost:3000/api/integrations/upwork/callback" +UPWORK_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/upwork" + +# Hubstaff Integration Configuration +HUBSTAFF_CLIENT_ID=XXXXXXX +HUBSTAFF_CLIENT_SECRET=XXXXXXX +HUBSTAFF_REDIRECT_URL="http://localhost:3000/api/integration/hubstaff/callback" +HUBSTAFF_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/hubstaff" # File System: LOCAL | S3 | WASABI | CLOUDINARY FILE_PROVIDER=LOCAL @@ -228,3 +256,15 @@ UNLEASH_INSTANCE_ID= UNLEASH_REFRESH_INTERVAL=15000 UNLEASH_METRICS_INTERVAL=60000 UNLEASH_API_KEY= + +# GitHub App Integration +GITHUB_INTEGRATION_APP_ID= +GITHUB_INTEGRATION_CLIENT_ID= +GITHUB_INTEGRATION_CLIENT_SECRET= +GITHUB_INTEGRATION_PRIVATE_KEY= +GITHUB_INTEGRATION_WEBHOOK_SECRET= + +# HubStaff Integration +HUBSTAFF_CLIENT_ID= +HUBSTAFF_CLIENT_SECRET= +HUBSTAFF_PERSONAL_ACCESS_TOKEN= diff --git a/.env.sample b/.env.sample index a21db48d762..a1437450581 100644 --- a/.env.sample +++ b/.env.sample @@ -62,10 +62,25 @@ FACEBOOK_CLIENT_SECRET=XXXXXXX FACEBOOK_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback FACEBOOK_GRAPH_VERSION=v3.0 +# Github OAuth Integration GITHUB_CLIENT_ID=XXXXXXX GITHUB_CLIENT_SECRET=XXXXXXX GITHUB_CALLBACK_URL=http://localhost:3000/api/auth/github/callback +# Github App Install Integration +GAUZY_GITHUB_APP_NAME= +GAUZY_GITHUB_APP_ID=XXXXXXX +GAUZY_GITHUB_APP_PRIVATE_KEY= + +# Github Webhook Configuration +GAUZY_GITHUB_WEBHOOK_URL=http://localhost:3000/api/auth/github/webhook +GAUZY_GITHUB_WEBHOOK_SECRET=XXXXXXX + +# Github Redirect URL +GAUZY_GITHUB_REDIRECT_URL=http://localhost:3000/api/integration/github/callback +GAUZY_GITHUB_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/github/setup/installation" +GAUZY_GITHUB_API_VERSION="2022-11-28" + LINKEDIN_CLIENT_ID=XXXXXXX LINKEDIN_CLIENT_SECRET=XXXXXXX LINKEDIN_CALLBACK_URL=http://localhost:3000/api/auth/linked/callback @@ -90,7 +105,18 @@ KEYCLOAK_AUTH_SERVER_URL=XXXXXXX KEYCLOAK_COOKIE_KEY=XXXXXXX INTEGRATED_HUBSTAFF_USER_PASS=hubstaffPassword -UPWORK_REDIRECT_URL=http://localhost:3000/api/integrations/upwork + +# Upwork Integration Config +UPWORK_API_KEY=XXXXXXX +UPWORK_API_SECRET=XXXXXXX +UPWORK_REDIRECT_URL="http://localhost:3000/api/integrations/upwork/callback" +UPWORK_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/upwork" + +# Hubstaff Integration Configuration +HUBSTAFF_CLIENT_ID=XXXXXXX +HUBSTAFF_CLIENT_SECRET=XXXXXXX +HUBSTAFF_REDIRECT_URL="http://localhost:3000/api/integration/hubstaff/callback" +HUBSTAFF_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/hubstaff" # File System: LOCAL | S3 | WASABI | CLOUDINARY FILE_PROVIDER=LOCAL @@ -263,3 +289,15 @@ FEATURE_EMAIL_VERIFICATION=false APPLE_ID= APPLE_ID_APP_PASSWORD= CSC_LINK= + +# GitHub App Integration +GITHUB_INTEGRATION_APP_ID= +GITHUB_INTEGRATION_CLIENT_ID= +GITHUB_INTEGRATION_CLIENT_SECRET= +GITHUB_INTEGRATION_PRIVATE_KEY= +GITHUB_INTEGRATION_WEBHOOK_SECRET= + +# HubStaff Integration +HUBSTAFF_CLIENT_ID= +HUBSTAFF_CLIENT_SECRET= +HUBSTAFF_PERSONAL_ACCESS_TOKEN= diff --git a/.github/workflows/desktop-app-prod.yml b/.github/workflows/desktop-app-prod.yml index 1c8856ef0f9..45f68f15a0a 100644 --- a/.github/workflows/desktop-app-prod.yml +++ b/.github/workflows/desktop-app-prod.yml @@ -66,6 +66,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-mac: runs-on: ${{ matrix.os }} @@ -115,6 +116,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-windows: runs-on: ${{ matrix.os }} @@ -164,3 +166,4 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true diff --git a/.github/workflows/desktop-app-stage.yml b/.github/workflows/desktop-app-stage.yml index c4e91bca88c..db2f33480eb 100644 --- a/.github/workflows/desktop-app-stage.yml +++ b/.github/workflows/desktop-app-stage.yml @@ -66,6 +66,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-mac: runs-on: ${{ matrix.os }} @@ -115,6 +116,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-windows: runs-on: ${{ matrix.os }} @@ -164,3 +166,4 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true diff --git a/.github/workflows/desktop-timer-app-prod.yml b/.github/workflows/desktop-timer-app-prod.yml index 8a1dfe7caee..79783176c1e 100644 --- a/.github/workflows/desktop-timer-app-prod.yml +++ b/.github/workflows/desktop-timer-app-prod.yml @@ -66,6 +66,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-mac: runs-on: ${{ matrix.os }} @@ -115,6 +116,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-windows: runs-on: ${{ matrix.os }} @@ -164,3 +166,4 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true diff --git a/.github/workflows/desktop-timer-app-stage.yml b/.github/workflows/desktop-timer-app-stage.yml index fa18e0a5d38..66f4d4c0ead 100644 --- a/.github/workflows/desktop-timer-app-stage.yml +++ b/.github/workflows/desktop-timer-app-stage.yml @@ -66,6 +66,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-mac: runs-on: ${{ matrix.os }} @@ -115,6 +116,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-windows: runs-on: ${{ matrix.os }} @@ -164,3 +166,4 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true diff --git a/.github/workflows/server-prod.yml b/.github/workflows/server-prod.yml index f1b72b827b7..24d5aa043ee 100644 --- a/.github/workflows/server-prod.yml +++ b/.github/workflows/server-prod.yml @@ -66,6 +66,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-mac: runs-on: ${{ matrix.os }} @@ -115,6 +116,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-windows: runs-on: ${{ matrix.os }} @@ -164,3 +166,4 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true diff --git a/.github/workflows/server-stage.yml b/.github/workflows/server-stage.yml index 33b09705672..ce461c9edc7 100644 --- a/.github/workflows/server-stage.yml +++ b/.github/workflows/server-stage.yml @@ -66,6 +66,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-mac: runs-on: ${{ matrix.os }} @@ -115,6 +116,7 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true release-windows: runs-on: ${{ matrix.os }} @@ -164,3 +166,4 @@ jobs: SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}' DO_KEY_ID: ${{ secrets.DO_KEY_ID }} DO_SECRET_KEY: ${{ secrets.DO_SECRET_KEY }} + NX_NO_CLOUD: true diff --git a/.scripts/configure.ts b/.scripts/configure.ts index 09a1833909c..c95889e8c11 100644 --- a/.scripts/configure.ts +++ b/.scripts/configure.ts @@ -142,6 +142,11 @@ if (!env.IS_DOCKER) { GAUZY_CLOUD_APP: '${env.GAUZY_CLOUD_APP}', FILE_PROVIDER: '${env.FILE_PROVIDER}', + + GAUZY_GITHUB_APP_NAME: '${env.GAUZY_GITHUB_APP_NAME}', + GAUZY_GITHUB_APP_ID: '${env.GAUZY_GITHUB_APP_ID}', + GAUZY_GITHUB_CLIENT_ID: '${env.GAUZY_GITHUB_CLIENT_ID}', + GAUZY_GITHUB_REDIRECT_URL: '${env.GAUZY_GITHUB_REDIRECT_URL}', }; `; } else { @@ -240,6 +245,11 @@ if (!env.IS_DOCKER) { GAUZY_CLOUD_APP: 'DOCKER_GAUZY_CLOUD_APP', FILE_PROVIDER: '${env.FILE_PROVIDER}', + + GAUZY_GITHUB_APP_NAME: '${env.GAUZY_GITHUB_APP_NAME}', + GAUZY_GITHUB_APP_ID: '${env.GAUZY_GITHUB_APP_ID}', + GAUZY_GITHUB_CLIENT_ID: '${env.GAUZY_GITHUB_CLIENT_ID}', + GAUZY_GITHUB_REDIRECT_URL: '${env.GAUZY_GITHUB_REDIRECT_URL}', }; `; } @@ -266,10 +276,10 @@ if (!isProd) { // we always want first to remove old generated files (one of them is not needed for current build) try { unlinkSync(`./apps/gauzy/src/environments/environment.ts`); -} catch {} +} catch { } try { unlinkSync(`./apps/gauzy/src/environments/environment.prod.ts`); -} catch {} +} catch { } const envFileDest: string = isProd ? 'environment.prod.ts' : 'environment.ts'; const envFileDestOther: string = !isProd diff --git a/.scripts/env.ts b/.scripts/env.ts index 6d4a1e190dd..fdc1240e1ce 100644 --- a/.scripts/env.ts +++ b/.scripts/env.ts @@ -65,6 +65,11 @@ export type Env = Readonly<{ GAUZY_CLOUD_APP: string; FILE_PROVIDER: string; + + GAUZY_GITHUB_APP_NAME: string; + GAUZY_GITHUB_APP_ID: string; + GAUZY_GITHUB_CLIENT_ID: string; + GAUZY_GITHUB_REDIRECT_URL: string; }>; export const env: Env = cleanEnv( @@ -118,6 +123,11 @@ export const env: Env = cleanEnv( GAUZY_CLOUD_APP: str({ default: 'https://app.gauzy.co/#' }), FILE_PROVIDER: str({ default: 'LOCAL' }), + + GAUZY_GITHUB_APP_NAME: str({ default: '' }), + GAUZY_GITHUB_APP_ID: str({ default: '' }), + GAUZY_GITHUB_CLIENT_ID: str({ default: '' }), + GAUZY_GITHUB_REDIRECT_URL: str({ default: '' }), }, { strict: true, dotEnvPath: __dirname + '/../.env' } ); diff --git a/README.md b/README.md index 32d9b0c845a..464518d667d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It's built with React / ReactNative (Expo) stack and connects to headless Ever G ## 🌟 What is it -[Ever® Gauzy™][uri_gauzy] - **Open-Source Business Management Platform** for On-Demand and Sharing Economies. +[Ever® Gauzy™][uri_gauzy] - **Open Business Management Platform** for Collaborative, On-Demand and Sharing Economies. - **Enterprise Resource Planning** (ERP) software. - **Customer Relationship Management** (CRM) software. @@ -25,7 +25,7 @@ It's built with React / ReactNative (Expo) stack and connects to headless Ever G ![overview](https://docs.gauzy.co/docs/assets/overview.png) -Ever® Gauzy™ Platform is a part of our larger Open Platform for **On-Demand and Sharing Economies** - [Ever® Platform™](https://ever.co). +Ever® Gauzy™ Platform is a part of our larger Open Platform for **Collaborative, On-Demand and Sharing Economies** - [Ever® Platform™](https://ever.co). ## ✨ Features @@ -305,6 +305,10 @@ You can also view a full list of our [contributors tracked by Github](https://gi +## ⭐ Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=ever-co/ever-gauzy&type=Date)](https://star-history.com/#ever-co/ever-gauzy&Date) + ## ❤️ Powered By

diff --git a/apps/api/src/assets/seed/integrations/github.svg b/apps/api/src/assets/seed/integrations/github.svg new file mode 100644 index 00000000000..dae0903d050 --- /dev/null +++ b/apps/api/src/assets/seed/integrations/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/api/src/assets/seed/integrations/jira.svg b/apps/api/src/assets/seed/integrations/jira.svg new file mode 100644 index 00000000000..20eb4f1b918 --- /dev/null +++ b/apps/api/src/assets/seed/integrations/jira.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index a5a41a298da..2db04e9ee4d 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -28,11 +28,13 @@ "../../../packages/plugins/knowledge-base", "../../../packages/plugins/changelog", "../../../packages/common", - "../../../packages/plugins/integration-ai", "../../../packages/config", "../../../packages/contracts", + "../../../packages/plugins/integration-ai", "../../../packages/plugins/integration-hubstaff", "../../../packages/plugins/integration-upwork", + "../../../packages/plugins/integration-github", + "../../../packages/plugins/integration-jira", "../../../packages/plugin" ] }, diff --git a/apps/gauzy-e2e/package.json b/apps/gauzy-e2e/package.json index 2bddf0d5397..55fb9017f2b 100644 --- a/apps/gauzy-e2e/package.json +++ b/apps/gauzy-e2e/package.json @@ -38,15 +38,17 @@ "build:package:plugin:knowledge-base": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/knowledge-base build", "build:package:core": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/core build", "build:package:auth": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/auth build", - "build:package:plugins": "yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:product-reviews", + "build:package:plugins": "yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:integration-jira && yarn run build:package:plugin:product-reviews", "build": "yarn build:package:all && concurrently --raw \"yarn build:api\" \"yarn build:gauzy\"", "build:api": "yarn ng build api", "build:gauzy": "yarn run postinstall.web && yarn run config:dev && yarn ng build gauzy", "build:package:plugin:integration-ai": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-ai build", "build:package:plugin:integration-hubstaff": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-hubstaff build", - "build:package:desktop-window": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/desktop-window build", - "build:package:plugin:product-reviews": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/product-reviews build", "build:package:plugin:integration-upwork": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-upwork build", + "build:package:plugin:integration-github": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-github build", + "build:package:plugin:integration-jira": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-jira build", + "build:package:plugin:product-reviews": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/product-reviews build", + "build:package:desktop-window": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/desktop-window build", "postinstall.web": "yarn node ./decorate-angular-cli.js && yarn npx ngcc --properties es2016 browser module main --first-only --create-ivy-entry-points && yarn node tools/web/postinstall" }, "resolutions": { diff --git a/apps/gauzy/src/app/@core/services/gauzy-ai/gauzy-ai.service.ts b/apps/gauzy/src/app/@core/services/gauzy-ai/gauzy-ai.service.ts index b4d81c85bee..72c65f3b884 100644 --- a/apps/gauzy/src/app/@core/services/gauzy-ai/gauzy-ai.service.ts +++ b/apps/gauzy/src/app/@core/services/gauzy-ai/gauzy-ai.service.ts @@ -19,6 +19,6 @@ export class GauzyAIService { * @returns */ addIntegration(input: IIntegrationKeySecretPairInput): Observable { - return this._http.post(`${API_PREFIX}/integrations/gauzy-ai`, input); + return this._http.post(`${API_PREFIX}/integration/gauzy-ai`, input); } } diff --git a/apps/gauzy/src/app/@core/services/github/github.service.ts b/apps/gauzy/src/app/@core/services/github/github.service.ts new file mode 100644 index 00000000000..aa939cdb073 --- /dev/null +++ b/apps/gauzy/src/app/@core/services/github/github.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { IBasePerTenantAndOrganizationEntityModel, IGithubAppInstallInput, IGithubIssue, IGithubRepositoryResponse, IIntegrationTenant } from '@gauzy/contracts'; +import { Observable, firstValueFrom } from 'rxjs'; +import { toParams } from '@gauzy/common-angular'; +import { API_PREFIX } from '../../constants'; + +@Injectable({ + providedIn: 'root', +}) +export class GithubService { + + constructor( + private readonly _http: HttpClient + ) { } + + /** + * Add a GitHub app installation. + * @param input The input data for the GitHub app installation. + * @returns A promise that resolves to the integration tenant object. + */ + async addInstallationApp(input: IGithubAppInstallInput): Promise { + const url = `${API_PREFIX}/integration/github/install`; + return firstValueFrom(this._http.post(url, input)); + } + + /** + * Get GitHub repositories for a specific integration. + * + * @param {string} integrationId - The ID of the integration. + * @param {IBasePerTenantAndOrganizationEntityModel} query - Query parameters for the request. + * @returns {Observable} An observable that emits GitHub repositories. + */ + getRepositories( + integrationId: IIntegrationTenant['id'], + query: IBasePerTenantAndOrganizationEntityModel + ): Observable { + const url = `${API_PREFIX}/integration/github/${integrationId}/repositories`; + const params = toParams(query); + + return this._http.get(url, { params }); + } + + /** + * Get GitHub repository issues for a specific integration, owner, and repository. + * + * @param {string} integrationId - The ID of the integration. + * @param {string} owner - The owner (username or organization) of the repository. + * @param {string} repo - The name of the repository. + * @param {IBasePerTenantAndOrganizationEntityModel} query - Query parameters for the request. + * @returns {Observable} An observable that emits GitHub issues. + */ + getRepositoryIssues( + integrationId: IIntegrationTenant['id'], + owner: string, + repo: string, + query: IBasePerTenantAndOrganizationEntityModel + ): Observable { + const url = `${API_PREFIX}/integration/github/${integrationId}/${owner}/${repo}/issues`; + const params = toParams(query); + + return this._http.get(url, { params }); + } +} diff --git a/apps/gauzy/src/app/@core/services/index.ts b/apps/gauzy/src/app/@core/services/index.ts index bb4c70f1d6b..44349ff90cb 100644 --- a/apps/gauzy/src/app/@core/services/index.ts +++ b/apps/gauzy/src/app/@core/services/index.ts @@ -128,3 +128,4 @@ export * from './users.service'; export * from './warehouse.service'; export * from './organization-task-setting.service'; export * from './gauzy-ai/gauzy-ai.service'; +export * from './github/github.service'; diff --git a/apps/gauzy/src/app/@core/services/integrations-store.service.ts b/apps/gauzy/src/app/@core/services/integrations-store.service.ts index e40b5ede677..bb87c38f6b5 100644 --- a/apps/gauzy/src/app/@core/services/integrations-store.service.ts +++ b/apps/gauzy/src/app/@core/services/integrations-store.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { IIntegrationViewModel, IIntegrationFilter, - IntegrationTypeNameEnum, + IntegrationTypeEnum, IntegrationTypeGroupEnum } from '@gauzy/contracts'; import { BehaviorSubject, Observable, of } from 'rxjs'; @@ -132,7 +132,7 @@ export class IntegrationsStoreService { ({ groupName }) => groupName === IntegrationTypeGroupEnum.FEATURED ); return featuredGroup.integrationTypes.find( - (item) => item.name === IntegrationTypeNameEnum.ALL_INTEGRATIONS + (item) => item.name === IntegrationTypeEnum.ALL_INTEGRATIONS ); } diff --git a/apps/gauzy/src/app/@core/utils/color-adapter.ts b/apps/gauzy/src/app/@core/utils/color-adapter.ts new file mode 100644 index 00000000000..6abcfa9ffae --- /dev/null +++ b/apps/gauzy/src/app/@core/utils/color-adapter.ts @@ -0,0 +1,49 @@ +import { Color, rgbString } from '@kurkle/color'; + +export class ColorAdapter { + public static hex2Rgb(hex: string) { + hex = this.normalize(hex); + return rgbString({ + r: parseInt(hex.slice(1, 3), 16), + g: parseInt(hex.slice(3, 5), 16), + b: parseInt(hex.slice(5, 7), 16), + a: 1, + }); + } + + public static normalize(hex: string): string { + const regex = /^#[0-9A-F]{6}$/i; + if (regex.test(hex)) { + return hex; + } else { + hex = '#' + hex; + return regex.test(hex) ? hex : '#000000'; + } + } + + public static contrast(bgColor: string) { + let color = new Color(bgColor); + color = color.valid ? color : new Color(this.hex2Rgb(bgColor)); + const MIN_THRESHOLD = 128; + const MAX_THRESHOLD = 186; + const contrast = color.rgb + ? color.rgb.r * 0.299 + color.rgb.g * 0.587 + color.rgb.b * 0.114 + : null; + if (contrast < MIN_THRESHOLD) { + return '#ffffff'; + } else if (contrast > MAX_THRESHOLD) { + return '#000000'; + } + } + + public static background(bgColor: string) { + const color = new Color(bgColor); + return color.valid ? bgColor : this.normalize(bgColor); + } + + public static hexToHsl(hexColor: string): string { + let color = new Color(hexColor); + color = color.valid ? color : new Color(this.hex2Rgb(hexColor)); + return color.hslString(); + } +} diff --git a/apps/gauzy/src/app/@core/utils/index.ts b/apps/gauzy/src/app/@core/utils/index.ts index 5606da56f19..7239b215f58 100644 --- a/apps/gauzy/src/app/@core/utils/index.ts +++ b/apps/gauzy/src/app/@core/utils/index.ts @@ -3,11 +3,13 @@ import { AnalyticsService } from './analytics.service'; import { PlayerService } from './player.service'; import { StateService } from './state.service'; import { SeoService } from './seo.service'; +import { ColorAdapter } from './color-adapter'; export { - LayoutService, - AnalyticsService, - PlayerService, - SeoService, - StateService + LayoutService, + AnalyticsService, + PlayerService, + SeoService, + StateService, + ColorAdapter, }; diff --git a/apps/gauzy/src/app/@shared/project-select/project/project.component.html b/apps/gauzy/src/app/@shared/project-select/project/project.component.html index d44fa021a2e..43209ef665b 100644 --- a/apps/gauzy/src/app/@shared/project-select/project/project.component.html +++ b/apps/gauzy/src/app/@shared/project-select/project/project.component.html @@ -1,3 +1,4 @@ + { return { ngModule: SharedModule, - providers: [] + providers: [], }; } } diff --git a/apps/gauzy/src/app/@shared/table-components/status-view/status-view.component.html b/apps/gauzy/src/app/@shared/table-components/status-view/status-view.component.html index c11c4612ed8..fe142781931 100644 --- a/apps/gauzy/src/app/@shared/table-components/status-view/status-view.component.html +++ b/apps/gauzy/src/app/@shared/table-components/status-view/status-view.component.html @@ -1,6 +1,14 @@ - + + + + + + diff --git a/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.html b/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.html index 3ea2ef1ecb5..eaaadf49b3e 100644 --- a/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.html +++ b/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.html @@ -49,8 +49,10 @@

@@ -99,7 +101,9 @@
[selected]="selectedTeams" (selectedChange)="onTeamsSelected($event)" fullWidth - [placeholder]="'FORM.PLACEHOLDERS.CHOOSE_TEAMS' | translate" + [placeholder]=" + 'FORM.PLACEHOLDERS.CHOOSE_TEAMS' | translate + " > @@ -148,7 +154,7 @@
@@ -238,7 +244,11 @@
- + diff --git a/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.ts b/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.ts index fdd78d7cb4c..8d20459aa6f 100644 --- a/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.ts +++ b/apps/gauzy/src/app/@shared/tasks/add-task-dialog/add-task-dialog.component.ts @@ -1,16 +1,16 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { - ITask, - IOrganizationProject, IEmployee, + IOrganization, + IOrganizationProject, IOrganizationTeam, + ISelectedEmployee, ITag, + ITask, TaskParticipantEnum, - IOrganization, TaskStatusEnum, - ISelectedEmployee } from '@gauzy/contracts'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NbDialogRef } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; import * as moment from 'moment'; @@ -34,9 +34,10 @@ import { richTextCKEditorConfig } from '../../ckeditor.config'; templateUrl: './add-task-dialog.component.html', styleUrls: ['./add-task-dialog.component.scss'], }) -export class AddTaskDialogComponent extends TranslationBaseComponent - implements OnInit { - +export class AddTaskDialogComponent + extends TranslationBaseComponent + implements OnInit +{ employees: IEmployee[] = []; teams: IOrganizationTeam[] = []; selectedMembers: string[] = []; @@ -46,25 +47,24 @@ export class AddTaskDialogComponent extends TranslationBaseComponent taskParticipantEnum = TaskParticipantEnum; participants = TaskParticipantEnum.EMPLOYEES; public ckConfig: CKEditor4.Config = richTextCKEditorConfig; - @Input() createTask = false; - - /* - * Getter & Setter for task - */ - _task: ITask; - get task(): ITask { - return this._task; - } - @Input() set task(value: ITask) { - this.selectedTask = value; - this._task = value; - } - /* * Payment Mutation Form */ public form: FormGroup = AddTaskDialogComponent.buildForm(this.fb); + + constructor( + public readonly dialogRef: NbDialogRef, + private readonly fb: FormBuilder, + private readonly store: Store, + public readonly translateService: TranslateService, + private readonly employeesService: EmployeesService, + private readonly tasksService: TasksService, + private readonly organizationTeamsService: OrganizationTeamsService + ) { + super(translateService); + } + static buildForm(fb: FormBuilder): FormGroup { return fb.group({ number: [{ value: '', disabled: true }], @@ -82,23 +82,30 @@ export class AddTaskDialogComponent extends TranslationBaseComponent description: [], tags: [], teams: [], + taskStatus: [], + taskSize: [], + taskPriority: [], }); } - constructor( - public readonly dialogRef: NbDialogRef, - private readonly fb: FormBuilder, - private readonly store: Store, - public readonly translateService: TranslateService, - private readonly employeesService: EmployeesService, - private readonly tasksService: TasksService, - private readonly organizationTeamsService: OrganizationTeamsService - ) { - super(translateService); + /* + * Getter & Setter for task + */ + _task: ITask; + + get task(): ITask { + return this._task; + } + + @Input() set task(value: ITask) { + this.selectedTask = value; + this._task = value; } ngOnInit() { - this.ckConfig.editorplaceholder = this.translateService.instant('FORM.PLACEHOLDERS.DESCRIPTION'); + this.ckConfig.editorplaceholder = this.translateService.instant( + 'FORM.PLACEHOLDERS.DESCRIPTION' + ); const storeOrganization$ = this.store.selectedOrganization$; const storeEmployee$ = this.store.selectedEmployee$; const storeProject$ = this.store.selectedProject$; @@ -161,7 +168,10 @@ export class AddTaskDialogComponent extends TranslationBaseComponent teams, title, priority, - size + size, + taskStatus, + taskSize, + taskPriority, } = this.selectedTask; const duration = moment.duration(estimate, 'seconds'); @@ -186,6 +196,9 @@ export class AddTaskDialogComponent extends TranslationBaseComponent description, tags, teams: this.selectedTeams, + taskStatus, + taskSize, + taskPriority, }); } } @@ -206,7 +219,15 @@ export class AddTaskDialogComponent extends TranslationBaseComponent .map((id) => this.teams.find((e) => e.id === id)) .filter((e) => !!e) ); - + this.form + .get('status') + .setValue(this.form.get('taskStatus').value?.name); + this.form + .get('priority') + .setValue(this.form.get('taskPriority').value?.name); + this.form + .get('size') + .setValue(this.form.get('taskSize').value?.name); const { estimateDays, estimateHours, estimateMinutes } = this.form.value; diff --git a/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.html b/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.html new file mode 100644 index 00000000000..f8fc522a16a --- /dev/null +++ b/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.html @@ -0,0 +1,14 @@ +
+
+
+
+ badge +
+
{{ name | replace : '-' : ' ' | titlecase }}
+
+
+
diff --git a/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.scss b/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.scss new file mode 100644 index 00000000000..a76cfac5321 --- /dev/null +++ b/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.scss @@ -0,0 +1,18 @@ +.badge-color { + display: flex; + padding: 4px; + width: fit-content; + line-height: 1; + border-radius: var(--border-radius); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + gap: 4px; + align-items: center; + font-weight: 600; + + .badge-img { + width: 18px; + height: 100%; + } +} diff --git a/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.ts b/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.ts new file mode 100644 index 00000000000..2a8e0524e67 --- /dev/null +++ b/apps/gauzy/src/app/@shared/tasks/task-badge-view/task-badge-view.component.ts @@ -0,0 +1,47 @@ +import { Component, Input } from '@angular/core'; +import { ITaskPriority, ITaskSize, ITaskStatus } from '@gauzy/contracts'; +import { ColorAdapter } from '../../../@core'; + +export type ITaskBadge = ITaskStatus | ITaskSize | ITaskPriority; + +@Component({ + selector: 'gauzy-task-badge-view', + templateUrl: './task-badge-view.component.html', + styleUrls: ['./task-badge-view.component.scss'], +}) +export class TaskBadgeViewComponent { + constructor() { + this._taskBadge = null; + } + + private _taskBadge: ITaskBadge; + + public get taskBadge(): ITaskBadge { + return this._taskBadge; + } + + @Input() + public set taskBadge(value: ITaskBadge) { + this._taskBadge = value; + } + + public get textColor() { + return ColorAdapter.contrast(this.taskBadge.color); + } + + public get backgroundColor() { + return ColorAdapter.background(this.taskBadge.color); + } + + public get icon() { + return this.taskBadge.fullIconUrl; + } + + public get name() { + return this.taskBadge.name; + } + + public get imageFilter() { + return ColorAdapter.hexToHsl(this.taskBadge.color); + } +} diff --git a/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.html b/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.html index 3a67a819f6c..e1bce604aa1 100644 --- a/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.html +++ b/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.html @@ -1,17 +1,19 @@ - - {{ item.name | replace: '_':' ' | replace: '-':' ' | titlecase }} - + + + + + + diff --git a/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.ts b/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.ts index 515ec72f995..318f75c9eab 100644 --- a/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.ts +++ b/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.component.ts @@ -1,12 +1,12 @@ import { + AfterViewInit, Component, - OnInit, - OnDestroy, - Input, - forwardRef, EventEmitter, + forwardRef, + Input, + OnDestroy, + OnInit, Output, - AfterViewInit, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; @@ -19,11 +19,15 @@ import { IPagination, ITaskPriority, ITaskPriorityFindInput, - TaskPriorityEnum + TaskPriorityEnum, } from '@gauzy/contracts'; import { distinctUntilChange, sluggable } from '@gauzy/common-angular'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { Store, TaskPrioritiesService, ToastrService } from '../../../@core/services'; +import { + Store, + TaskPrioritiesService, + ToastrService, +} from '../../../@core/services'; import { TranslationBaseComponent } from '../../language-base/translation-base.component'; @UntilDestroy({ checkProperties: true }) @@ -38,54 +42,73 @@ import { TranslationBaseComponent } from '../../language-base/translation-base.c }, ], }) -export class TaskPrioritySelectComponent extends TranslationBaseComponent - implements AfterViewInit, OnInit, OnDestroy { - +export class TaskPrioritySelectComponent + extends TranslationBaseComponent + implements AfterViewInit, OnInit, OnDestroy +{ private subject$: Subject = new Subject(); - public organization: IOrganization; - public priorities$: BehaviorSubject = new BehaviorSubject([]); - /** * Default global task priorities */ - private _priorities: Array<{ name: string; value: TaskPriorityEnum & any }> = [ + private _priorities: Array<{ + name: string; + value: TaskPriorityEnum & any; + }> = [ { name: TaskPriorityEnum.URGENT, - value: sluggable(TaskPriorityEnum.URGENT) + value: sluggable(TaskPriorityEnum.URGENT), }, { name: TaskPriorityEnum.HIGH, - value: sluggable(TaskPriorityEnum.HIGH) + value: sluggable(TaskPriorityEnum.HIGH), }, { name: TaskPriorityEnum.MEDIUM, - value: sluggable(TaskPriorityEnum.MEDIUM) + value: sluggable(TaskPriorityEnum.MEDIUM), }, { name: TaskPriorityEnum.LOW, - value: sluggable(TaskPriorityEnum.LOW) + value: sluggable(TaskPriorityEnum.LOW), }, ]; + public organization: IOrganization; + public priorities$: BehaviorSubject = new BehaviorSubject( + [] + ); + @Output() onChanged = new EventEmitter(); + + constructor( + public readonly translateService: TranslateService, + public readonly store: Store, + public readonly taskPrioritiesService: TaskPrioritiesService, + private readonly toastrService: ToastrService + ) { + super(translateService); + } /* - * Getter & Setter for selected organization project - */ + * Getter & Setter for selected organization project + */ private _projectId: IOrganizationProject['id']; + get projectId(): IOrganizationProject['id'] { return this._projectId; } + @Input() set projectId(value: IOrganizationProject['id']) { this._projectId = value; this.subject$.next(true); } /* - * Getter & Setter for dynamic add tag option - */ + * Getter & Setter for dynamic add tag option + */ private _addTag: boolean = true; + get addTag(): boolean { return this._addTag; } + @Input() set addTag(value: boolean) { this._addTag = value; } @@ -94,9 +117,11 @@ export class TaskPrioritySelectComponent extends TranslationBaseComponent * Getter & Setter for dynamic placeholder */ private _placeholder: string; + get placeholder(): string { return this._placeholder; } + @Input() set placeholder(value: string) { this._placeholder = value; } @@ -104,29 +129,21 @@ export class TaskPrioritySelectComponent extends TranslationBaseComponent /* * Getter & Setter for priority */ - private _priority: TaskPriorityEnum | string; - set priority(val: TaskPriorityEnum | string) { + private _priority: ITaskPriority; + + get priority(): ITaskPriority { + return this._priority; + } + + set priority(val: ITaskPriority) { this._priority = val; this.onChange(val); this.onTouched(val); } - get priority(): TaskPriorityEnum | string { - return this._priority; - } - - onChange: any = () => { }; - onTouched: any = () => { }; - @Output() onChanged = new EventEmitter(); + onChange: any = () => {}; - constructor( - public readonly translateService: TranslateService, - public readonly store: Store, - public readonly taskPrioritiesService: TaskPrioritiesService, - private readonly toastrService: ToastrService - ) { - super(translateService); - } + onTouched: any = () => {}; ngOnInit(): void { this.subject$ @@ -155,8 +172,8 @@ export class TaskPrioritySelectComponent extends TranslationBaseComponent .subscribe(); } - writeValue(value: TaskPriorityEnum) { - this._priority = value; + writeValue(value: ITaskPriority) { + this.priority = value; } registerOnChange(fn: (rating: number) => void): void { @@ -167,8 +184,8 @@ export class TaskPrioritySelectComponent extends TranslationBaseComponent this.onTouched = fn; } - selectPriority(event: { label: string; value: TaskPriorityEnum }) { - this.onChanged.emit(event ? event.value : null); + selectPriority(priority: ITaskPriority) { + this.onChanged.emit(priority); } /** @@ -182,20 +199,26 @@ export class TaskPrioritySelectComponent extends TranslationBaseComponent const { tenantId } = this.store.user; const { id: organizationId } = this.organization; - this.taskPrioritiesService.get({ - tenantId, - organizationId, - ...(this.projectId - ? { - projectId: this.projectId - } - : {}), - }).pipe( - map(({ items, total }: IPagination) => total > 0 ? items : this._priorities), - tap((priorities: ITaskPriority[]) => this.priorities$.next(priorities)), - untilDestroyed(this) - ) - .subscribe(); + this.taskPrioritiesService + .get({ + tenantId, + organizationId, + ...(this.projectId + ? { + projectId: this.projectId, + } + : {}), + }) + .pipe( + map(({ items, total }: IPagination) => + total > 0 ? items : this._priorities + ), + tap((priorities: ITaskPriority[]) => + this.priorities$.next(priorities) + ), + untilDestroyed(this) + ) + .subscribe(); } /** @@ -218,13 +241,13 @@ export class TaskPrioritySelectComponent extends TranslationBaseComponent name, ...(this.projectId ? { - projectId: this.projectId - } + projectId: this.projectId, + } : {}), }); const priority: ITaskPriority = await firstValueFrom(source); - if (priority.value) { - this.priority = priority.value; + if (priority) { + this.priority = priority; } } catch (error) { this.toastrService.error(error); @@ -233,5 +256,5 @@ export class TaskPrioritySelectComponent extends TranslationBaseComponent } }; - ngOnDestroy(): void { } + ngOnDestroy(): void {} } diff --git a/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.module.ts b/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.module.ts index 4e306fbee65..7ee1a571153 100644 --- a/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.module.ts +++ b/apps/gauzy/src/app/@shared/tasks/task-priority-select/task-priority-select.module.ts @@ -8,21 +8,17 @@ import { TranslateModule } from '../../translate/translate.module'; import { SharedModule } from '../../shared.module'; @NgModule({ - declarations: [ - TaskPrioritySelectComponent - ], - exports: [ - TaskPrioritySelectComponent - ], + declarations: [TaskPrioritySelectComponent], + exports: [TaskPrioritySelectComponent], imports: [ CommonModule, FormsModule, TranslateModule, NgSelectModule, - SharedModule + SharedModule, + NgSelectModule, + NgSelectModule, ], - providers: [ - TaskPrioritiesService - ] + providers: [TaskPrioritiesService], }) -export class TaskPrioritySelectModule { } +export class TaskPrioritySelectModule {} diff --git a/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.html b/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.html index dca413320ec..881e192e4e5 100644 --- a/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.html +++ b/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.html @@ -1,17 +1,18 @@ - - {{ item.name | replace: '_':' ' | replace: '-':' ' | titlecase }} - + + + + + + diff --git a/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.ts b/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.ts index dae0a42c595..20f472c154d 100644 --- a/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.ts +++ b/apps/gauzy/src/app/@shared/tasks/task-size-select/task-size-select.component.ts @@ -19,11 +19,15 @@ import { IPagination, ITaskSize, ITaskSizeFindInput, - TaskSizeEnum + TaskSizeEnum, } from '@gauzy/contracts'; import { distinctUntilChange, sluggable } from '@gauzy/common-angular'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { Store, TaskSizesService, ToastrService } from '../../../@core/services'; +import { + Store, + TaskSizesService, + ToastrService, +} from '../../../@core/services'; import { TranslationBaseComponent } from '../../language-base/translation-base.component'; @UntilDestroy({ checkProperties: true }) @@ -38,9 +42,10 @@ import { TranslationBaseComponent } from '../../language-base/translation-base.c }, ], }) -export class TaskSizeSelectComponent extends TranslationBaseComponent - implements AfterViewInit, OnInit, OnDestroy { - +export class TaskSizeSelectComponent + extends TranslationBaseComponent + implements AfterViewInit, OnInit, OnDestroy +{ private subject$: Subject = new Subject(); public organization: IOrganization; public sizes$: BehaviorSubject = new BehaviorSubject([]); @@ -51,29 +56,29 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent private _sizes: Array<{ name: string; value: TaskSizeEnum & any }> = [ { name: TaskSizeEnum.X_LARGE, - value: sluggable(TaskSizeEnum.X_LARGE) + value: sluggable(TaskSizeEnum.X_LARGE), }, { name: TaskSizeEnum.LARGE, - value: sluggable(TaskSizeEnum.LARGE) + value: sluggable(TaskSizeEnum.LARGE), }, { name: TaskSizeEnum.MEDIUM, - value: sluggable(TaskSizeEnum.MEDIUM) + value: sluggable(TaskSizeEnum.MEDIUM), }, { name: TaskSizeEnum.SMALL, - value: sluggable(TaskSizeEnum.SMALL) + value: sluggable(TaskSizeEnum.SMALL), }, { name: TaskSizeEnum.TINY, - value: sluggable(TaskSizeEnum.TINY) + value: sluggable(TaskSizeEnum.TINY), }, ]; /* - * Getter & Setter for selected organization project - */ + * Getter & Setter for selected organization project + */ private _projectId: IOrganizationProject['id']; get projectId(): IOrganizationProject['id'] { return this._projectId; @@ -84,8 +89,8 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent } /* - * Getter & Setter for dynamic add tag option - */ + * Getter & Setter for dynamic add tag option + */ private _addTag: boolean = true; get addTag(): boolean { return this._addTag; @@ -108,20 +113,20 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent /* * Getter & Setter for size */ - private _size: TaskSizeEnum | string; - set size(val: TaskSizeEnum | string) { + private _size: ITaskSize; + set size(val: ITaskSize) { this._size = val; this.onChange(val); this.onTouched(val); } - get size(): TaskSizeEnum | string { + get size(): ITaskSize { return this._size; } - onChange: any = () => { }; - onTouched: any = () => { }; + onChange: any = () => {}; + onTouched: any = () => {}; - @Output() onChanged = new EventEmitter(); + @Output() onChanged = new EventEmitter(); constructor( public readonly translateService: TranslateService, @@ -159,8 +164,8 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent .subscribe(); } - writeValue(value: TaskSizeEnum) { - this._size = value; + writeValue(value: ITaskSize) { + this.size = value; } registerOnChange(fn: (rating: number) => void): void { @@ -171,8 +176,8 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent this.onTouched = fn; } - selectSize(event: { label: string; value: TaskSizeEnum }) { - this.onChanged.emit(event ? event.value : null); + selectSize(size: ITaskSize) { + this.onChanged.emit(size); } /** @@ -186,20 +191,24 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent const { tenantId } = this.store.user; const { id: organizationId } = this.organization; - this.taskSizesService.get({ - tenantId, - organizationId, - ...(this.projectId - ? { - projectId: this.projectId - } - : {}), - }).pipe( - map(({ items, total }: IPagination) => total > 0 ? items : this._sizes), - tap((sizes: ITaskSize[]) => this.sizes$.next(sizes)), - untilDestroyed(this) - ) - .subscribe(); + this.taskSizesService + .get({ + tenantId, + organizationId, + ...(this.projectId + ? { + projectId: this.projectId, + } + : {}), + }) + .pipe( + map(({ items, total }: IPagination) => + total > 0 ? items : this._sizes + ), + tap((sizes: ITaskSize[]) => this.sizes$.next(sizes)), + untilDestroyed(this) + ) + .subscribe(); } /** @@ -222,13 +231,13 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent name, ...(this.projectId ? { - projectId: this.projectId - } + projectId: this.projectId, + } : {}), }); const size: ITaskSize = await firstValueFrom(source); if (size.value) { - this.size = size.value; + this.size = size; } } catch (error) { this.toastrService.error(error); @@ -237,5 +246,5 @@ export class TaskSizeSelectComponent extends TranslationBaseComponent } }; - ngOnDestroy(): void { } + ngOnDestroy(): void {} } diff --git a/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.html b/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.html index 2ba47508324..3b73d161388 100644 --- a/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.html +++ b/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.html @@ -1,11 +1,16 @@ - - {{ item.name | replace: '_':' ' | replace: '-':' ' | titlecase }} - + + + + + + diff --git a/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.ts b/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.ts index f3b43f7f626..45bf94a8745 100644 --- a/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.ts +++ b/apps/gauzy/src/app/@shared/tasks/task-status-select/task-status-select.component.ts @@ -1,22 +1,33 @@ import { + AfterViewInit, Component, - OnInit, - OnDestroy, - Input, - forwardRef, EventEmitter, + forwardRef, + Input, + OnDestroy, + OnInit, Output, - AfterViewInit, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { combineLatest, debounceTime, firstValueFrom, Subject } from 'rxjs'; import { filter, map, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; -import { IOrganization, IOrganizationProject, IPagination, ITaskStatus, ITaskStatusFindInput, TaskStatusEnum } from '@gauzy/contracts'; +import { + IOrganization, + IOrganizationProject, + IPagination, + ITaskStatus, + ITaskStatusFindInput, + TaskStatusEnum, +} from '@gauzy/contracts'; import { distinctUntilChange, sluggable } from '@gauzy/common-angular'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { TaskStatusesService, Store, ToastrService } from '../../../@core/services'; +import { + Store, + TaskStatusesService, + ToastrService, +} from '../../../@core/services'; import { TranslationBaseComponent } from '../../language-base/translation-base.component'; @UntilDestroy({ checkProperties: true }) @@ -31,13 +42,11 @@ import { TranslationBaseComponent } from '../../language-base/translation-base.c }, ], }) -export class TaskStatusSelectComponent extends TranslationBaseComponent - implements AfterViewInit, OnInit, OnDestroy { - +export class TaskStatusSelectComponent + extends TranslationBaseComponent + implements AfterViewInit, OnInit, OnDestroy +{ private subject$: Subject = new Subject(); - public organization: IOrganization; - public statuses$: BehaviorSubject = new BehaviorSubject([]); - /** * Default global task statuses */ @@ -67,26 +76,42 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent value: sluggable(TaskStatusEnum.COMPLETED), }, ]; + public organization: IOrganization; + public statuses$: BehaviorSubject = new BehaviorSubject([]); + @Output() onChanged = new EventEmitter(); + + constructor( + public readonly translateService: TranslateService, + public readonly store: Store, + public readonly taskStatusesService: TaskStatusesService, + private readonly toastrService: ToastrService + ) { + super(translateService); + } /* - * Getter & Setter for selected organization project - */ + * Getter & Setter for selected organization project + */ private _projectId: IOrganizationProject['id']; + get projectId(): IOrganizationProject['id'] { return this._projectId; } + @Input() set projectId(value: IOrganizationProject['id']) { this._projectId = value; this.subject$.next(true); } /* - * Getter & Setter for dynamic add tag option - */ + * Getter & Setter for dynamic add tag option + */ private _addTag: boolean = true; + get addTag(): boolean { return this._addTag; } + @Input() set addTag(value: boolean) { this._addTag = value; } @@ -95,9 +120,11 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent * Getter & Setter for dynamic placeholder */ private _placeholder: string; + get placeholder(): string { return this._placeholder; } + @Input() set placeholder(value: string) { this._placeholder = value; } @@ -105,29 +132,21 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent /* * Getter & Setter for status */ - private _status: TaskStatusEnum; - set status(val: TaskStatusEnum) { + private _status: ITaskStatus; + + get status(): ITaskStatus { + return this._status; + } + + set status(val: ITaskStatus) { this._status = val; this.onChange(val); this.onTouched(val); } - get status(): TaskStatusEnum { - return this._status; - } onChange: any = () => {}; - onTouched: any = () => {}; - - @Output() onChanged = new EventEmitter(); - constructor( - public readonly translateService: TranslateService, - public readonly store: Store, - public readonly taskStatusesService: TaskStatusesService, - private readonly toastrService: ToastrService - ) { - super(translateService); - } + onTouched: any = () => {}; ngOnInit(): void { this.subject$ @@ -156,8 +175,8 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent .subscribe(); } - writeValue(value: TaskStatusEnum) { - this._status = value; + writeValue(value: ITaskStatus) { + this.status = value; } registerOnChange(fn: (rating: number) => void): void { @@ -168,8 +187,8 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent this.onTouched = fn; } - selectStatus(event: { label: string; value: TaskStatusEnum }) { - this.onChanged.emit(event ? event.value : null); + selectStatus(status: ITaskStatus) { + this.onChanged.emit(status); } /** @@ -183,20 +202,24 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent const { tenantId } = this.store.user; const { id: organizationId } = this.organization; - this.taskStatusesService.get({ - tenantId, - organizationId, - ...(this.projectId - ? { - projectId: this.projectId - } - : {}), - }).pipe( - map(({ items, total }: IPagination) => total > 0 ? items : this._statuses), - tap((statuses: ITaskStatus[]) => this.statuses$.next(statuses)), - untilDestroyed(this) - ) - .subscribe(); + this.taskStatusesService + .get({ + tenantId, + organizationId, + ...(this.projectId + ? { + projectId: this.projectId, + } + : {}), + }) + .pipe( + map(({ items, total }: IPagination) => + total > 0 ? items : this._statuses + ), + tap((statuses: ITaskStatus[]) => this.statuses$.next(statuses)), + untilDestroyed(this) + ) + .subscribe(); } /** @@ -219,7 +242,7 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent name, ...(this.projectId ? { - projectId: this.projectId + projectId: this.projectId, } : {}), }); @@ -227,7 +250,7 @@ export class TaskStatusSelectComponent extends TranslationBaseComponent } catch (error) { this.toastrService.error(error); } finally { - this.subject$.next(true) + this.subject$.next(true); } }; diff --git a/apps/gauzy/src/app/@shared/team-select/team/team.component.html b/apps/gauzy/src/app/@shared/team-select/team/team.component.html index 89caaec97cc..8a5a686fd0f 100644 --- a/apps/gauzy/src/app/@shared/team-select/team/team.component.html +++ b/apps/gauzy/src/app/@shared/team-select/team/team.component.html @@ -1,7 +1,9 @@ + diff --git a/apps/gauzy/src/app/@shared/team-select/team/team.component.ts b/apps/gauzy/src/app/@shared/team-select/team/team.component.ts index 6023098aa49..c0d25d9c443 100644 --- a/apps/gauzy/src/app/@shared/team-select/team/team.component.ts +++ b/apps/gauzy/src/app/@shared/team-select/team/team.component.ts @@ -4,7 +4,6 @@ import { OnDestroy, Input, forwardRef, - AfterViewInit, Output, EventEmitter } from '@angular/core'; @@ -26,7 +25,6 @@ import { } from '@gauzy/common-angular'; import { ALL_TEAM_SELECTED } from './default-team'; import { - OrganizationTeamsService, Store, ToastrService @@ -48,7 +46,7 @@ import { OrganizationTeamStore } from '../../../@core/services/organization-team ] }) export class TeamSelectorComponent - implements OnInit, OnDestroy, AfterViewInit { + implements OnInit, OnDestroy { teams: IOrganizationTeam[] = []; selectedTeam: IOrganizationTeam; hasAddTeam$: Observable; @@ -61,6 +59,7 @@ export class TeamSelectorComponent @Input() shortened = false; @Input() disabled = false; @Input() multiple = false; + @Input() label = null; private _teamId: string | string[]; get teamId(): string | string[] { @@ -71,7 +70,7 @@ export class TeamSelectorComponent this.onChange(val); this.onTouched(val); } - //----------------------------------------------- + private _employeeId: string; public get employeeId() { return this._employeeId; diff --git a/apps/gauzy/src/app/@theme/components/gauzy-logo/gauzy-logo.component.ts b/apps/gauzy/src/app/@theme/components/gauzy-logo/gauzy-logo.component.ts index a40fca560e6..70f8ef305b6 100644 --- a/apps/gauzy/src/app/@theme/components/gauzy-logo/gauzy-logo.component.ts +++ b/apps/gauzy/src/app/@theme/components/gauzy-logo/gauzy-logo.component.ts @@ -90,7 +90,7 @@ export class GauzyLogoComponent implements AfterViewInit, OnInit, OnDestroy { { title: 'Integrations', icon: 'fas fa-swatchbook', - link: '/pages/integrations', + link: '/pages/integrations/new', pathMatch: 'prefix', data: { translationKey: 'MENU.INTEGRATIONS', @@ -99,7 +99,6 @@ export class GauzyLogoComponent implements AfterViewInit, OnInit, OnDestroy { } } ]; - settings = { title: 'Settings', icon: 'fas fa-cog', diff --git a/apps/gauzy/src/app/app-routing.module.ts b/apps/gauzy/src/app/app-routing.module.ts index be849873700..7e9cada813f 100644 --- a/apps/gauzy/src/app/app-routing.module.ts +++ b/apps/gauzy/src/app/app-routing.module.ts @@ -54,4 +54,4 @@ const config: ExtraOptions = { ], exports: [RouterModule] }) -export class AppRoutingModule {} +export class AppRoutingModule { } diff --git a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff-authorize/hubstaff-authorize.component.ts b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff-authorize/hubstaff-authorize.component.ts index cf1c79dace0..90efe1b588a 100644 --- a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff-authorize/hubstaff-authorize.component.ts +++ b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff-authorize/hubstaff-authorize.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { Validators, FormGroup, FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { filter, tap } from 'rxjs/operators'; -import { IIntegrationTenant, IOrganization, IntegrationEnum } from '@gauzy/contracts'; +import { IIntegration, IIntegrationTenant, IOrganization, IntegrationEnum } from '@gauzy/contracts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { HubstaffService, IntegrationsService, Store } from './../../../../@core/services'; @@ -113,7 +113,7 @@ export class HubstaffAuthorizeComponent implements OnInit, OnDestroy { /** * Hubstaff integration remember state API call */ - private _redirectToHubstaffIntegration(integrationId) { + private _redirectToHubstaffIntegration(integrationId: IIntegration['id']) { this._router.navigate(['pages/integrations/hubstaff', integrationId]); } diff --git a/apps/gauzy/src/app/pages/integrations/github/components/installation/installation.component.html b/apps/gauzy/src/app/pages/integrations/github/components/installation/installation.component.html new file mode 100644 index 00000000000..2df16eb37a6 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/installation/installation.component.html @@ -0,0 +1,10 @@ + + + + +
+

Installing. Please wait...

+
+
+
+
diff --git a/apps/gauzy/src/app/pages/integrations/github/components/installation/installation.component.ts b/apps/gauzy/src/app/pages/integrations/github/components/installation/installation.component.ts new file mode 100644 index 00000000000..f2e52f78e62 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/installation/installation.component.ts @@ -0,0 +1,75 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { filter, tap } from 'rxjs/operators'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { IGithubAppInstallInput, IOrganization } from '@gauzy/contracts'; +import { GithubService } from '../../../../../@core/services'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + templateUrl: './installation.component.html' +}) +export class GithubInstallationComponent implements OnInit { + + public isLoading: boolean = true; + public organization: IOrganization; + + constructor( + private readonly _route: ActivatedRoute, + private readonly _githubService: GithubService, + ) { } + + ngOnInit() { + this._route.queryParams + .pipe( + filter(({ installation_id, setup_action }) => !!installation_id && !!setup_action), + tap(async ({ installation_id, setup_action, state }: IGithubAppInstallInput) => + await this.verifyGitHubAppAuthorization({ + installation_id, + setup_action, + state + }) + ), + untilDestroyed(this) + ) + .subscribe() + } + + /** + * + * @param input + */ + async verifyGitHubAppAuthorization(input: IGithubAppInstallInput) { + const { installation_id, setup_action, state } = input; + if (installation_id && setup_action && state) { + const [organizationId, tenantId] = state.split('|'); + try { + await this._githubService.addInstallationApp({ + installation_id, + setup_action, + organizationId, + tenantId + }); + this.handleClosedPopupWindow(); + } catch (error) { + console.log('Error while install github app: %s', installation_id); + } + } + } + + /** + * Handle the case when the popup window is closed. + */ + private handleClosedPopupWindow() { + this.isLoading = false; + console.log('Popup window closed after github app installed!'); + + // Delay navigation by 2 seconds before close window + setTimeout(() => { + /** Close current window */ + window.opener = null; + window.open("", "_self"); + window.close(); + }, 2000); // 2000 milliseconds = 2 seconds + } +} diff --git a/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.html b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.html new file mode 100644 index 00000000000..4429c13d659 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.html @@ -0,0 +1,29 @@ + + +
{{ 'INTEGRATIONS.GITHUB_PAGE.NAME' | translate }}
+
+ +
+
+ + +
+
+
diff --git a/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.scss b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.scss new file mode 100644 index 00000000000..3d4120bac59 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.scss @@ -0,0 +1,21 @@ +:host { + nb-card, + nb-card-body { + background-color: var(--gauzy-card-2); + } +} +::ng-deep { + .issues-container { + .ng2-smart-actions { + text-align: center; + width: 5%; + .form-control { + width: 15px; + display: inline-block; + } + } + input { + box-shadow: none !important; + } + } +} diff --git a/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.spec.ts b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.spec.ts new file mode 100644 index 00000000000..0d0503049f9 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.spec.ts @@ -0,0 +1,24 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { GithubViewComponent } from './view.component'; + +describe('GithubViewComponent', () => { + let component: GithubViewComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [GithubViewComponent], + teardown: { destroyAfterEach: false } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GithubViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.ts b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.ts new file mode 100644 index 00000000000..2a8ff2b041e --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/view/view.component.ts @@ -0,0 +1,195 @@ +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { TitleCasePipe } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; +import { debounceTime, of } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { catchError, filter, map, switchMap, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { IGithubIssue, IGithubRepository, IGithubRepositoryResponse, IOrganization } from '@gauzy/contracts'; +import { distinctUntilChange } from '@gauzy/common-angular'; +import { TranslationBaseComponent } from './../../../../../@shared/language-base'; +import { + ErrorHandlingService, + GithubService, + Store +} from './../../../../../@core/services'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + styleUrls: ['./view.component.scss'], + templateUrl: './view.component.html', + providers: [ + TitleCasePipe + ] +}) +export class GithubViewComponent extends TranslationBaseComponent implements AfterViewInit, OnInit { + + public settingsSmartTable: object; + public organization: IOrganization; + public loading: boolean; + public repositories: IGithubRepository[] = []; + public repositories$: Observable; + public issues$: Observable; + public issues: any[] = []; + + constructor( + public readonly _translateService: TranslateService, + private readonly _activatedRoute: ActivatedRoute, + private readonly _titlecasePipe: TitleCasePipe, + private readonly _errorHandlingService: ErrorHandlingService, + private readonly _store: Store, + private readonly _githubService: GithubService, + ) { + super(_translateService); + } + + ngOnInit(): void { + this._loadSmartTableSettings(); + this._applyTranslationOnSmartTable(); + } + + ngAfterViewInit(): void { + this._store.selectedOrganization$ + .pipe( + debounceTime(200), + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => this.organization = organization), + tap(() => this.getRepositories()), + untilDestroyed(this) + ) + .subscribe(); + } + + /** + * Fetches repositories for a given integration and organization. + */ + private getRepositories() { + // Ensure there is a valid organization + if (!this.organization) { + return; + } + + this.loading = true; + // Extract organization properties + const { id: organizationId, tenantId } = this.organization; + this.repositories$ = this._activatedRoute.params.pipe( + // Get the 'integrationId' route parameter + switchMap(({ integrationId }) => { + return this._githubService.getRepositories(integrationId, { + organizationId, + tenantId + }); + }), + // Update component state with fetched repositories + tap(({ repositories }: IGithubRepositoryResponse) => { + this.repositories = repositories; + }), + map(({ repositories }) => repositories), + catchError((error) => { + // Handle and log errors + this._errorHandlingService.handleError(error); + return of([]); + }), + tap(() => { + this.loading = false; + }), + // Handle component lifecycle to avoid memory leaks + untilDestroyed(this), + ); + } + + /** + * + * @param repository + */ + selectRepository(repository: IGithubRepository) { + this.issues$ = repository ? this.getRepositoryIssue(repository) : of([]); + } + + private getRepositoryIssue(repository: IGithubRepository) { + // Ensure there is a valid organization + if (!this.organization) { + return; + } + const owner = repository.owner['login']; + const repo = repository.name; + + this.loading = true; + // Extract organization properties + const { id: organizationId, tenantId } = this.organization; + return this._activatedRoute.params.pipe( + // Get the 'integrationId' route parameter + switchMap(({ integrationId }) => { + return this._githubService.getRepositoryIssues(integrationId, owner, repo, { + organizationId, + tenantId, + }); + }), + // Update component state with fetched issues + tap((issues: IGithubIssue[]) => { + this.issues = issues; + }), + catchError((error) => { + // Handle and log errors + this._errorHandlingService.handleError(error); + return of([]); + }), + tap(() => { + this.loading = false; + }), + // Handle component lifecycle to avoid memory leaks + untilDestroyed(this), + ); + } + + /** + * + */ + private _applyTranslationOnSmartTable() { + this.translateService.onLangChange + .pipe( + tap(() => this._loadSmartTableSettings()), + untilDestroyed(this) + ) + .subscribe(); + } + + /** + * + */ + private _loadSmartTableSettings() { + this.settingsSmartTable = { + selectedRowIndex: -1, + selectMode: 'multi', + actions: { + add: false, + edit: false, + delete: false, + select: true + }, + columns: { + number: { + title: this.getTranslation('SM_TABLE.NUMBER'), + type: 'number', + valuePrepareFunction: (data: string) => { + console.log(data); + return data; + } + }, + title: { + title: this.getTranslation('SM_TABLE.TITLE'), + type: 'string' + }, + state: { + title: this.getTranslation('SM_TABLE.STATUS'), + type: 'string', + valuePrepareFunction: (data: string) => { + return this._titlecasePipe.transform(data); + } + } + } + }; + } +} diff --git a/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.html b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.html new file mode 100644 index 00000000000..2df16eb37a6 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.html @@ -0,0 +1,10 @@ + + + + +
+

Installing. Please wait...

+
+
+
+
diff --git a/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.scss b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.scss new file mode 100644 index 00000000000..0b27efbff7d --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.scss @@ -0,0 +1,6 @@ +:host { + nb-card, + nb-card-body { + background-color: var(--gauzy-card-2); + } +} diff --git a/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.spec.ts b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.spec.ts new file mode 100644 index 00000000000..825f7b073d3 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.spec.ts @@ -0,0 +1,24 @@ +import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { GithubWizardComponent } from './wizard.component'; + +describe('GithubWizardComponent', () => { + let component: GithubWizardComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [GithubWizardComponent], + teardown: { destroyAfterEach: false } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GithubWizardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.ts b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.ts new file mode 100644 index 00000000000..710da78b6e8 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/components/wizard/wizard.component.ts @@ -0,0 +1,167 @@ +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, Router } from '@angular/router'; +import { debounceTime, filter, tap } from 'rxjs/operators'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { environment } from '@env/environment'; +import { IOrganization } from '@gauzy/contracts'; +import { distinctUntilChange, toParams } from '@gauzy/common-angular'; +import { Store } from '../../../../../@core/services'; +import { GITHUB_AUTHORIZATION_URL } from '../../github.config'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + templateUrl: './wizard.component.html' +}) +export class GithubWizardComponent implements AfterViewInit, OnInit { + + public organization: IOrganization; + public isLoading: boolean = true; + // save a reference to the window so we can close it + private window = null; + + constructor( + private readonly _activatedRoute: ActivatedRoute, + private readonly _router: Router, + private readonly store: Store, + ) { } + + ngOnInit(): void { + this._activatedRoute.data + .pipe( + tap(({ integration }: Data) => { + if (integration) { + this._router.navigate(['/pages/integrations/github', integration.id]); + } + }), + untilDestroyed(this) + ) + .subscribe(); + } + + ngAfterViewInit(): void { + this.store.selectedOrganization$ + .pipe( + debounceTime(200), + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => this.organization = organization), + tap(() => this.githubAppInstallation()), + untilDestroyed(this) + ) + .subscribe(); + } + + /** + * Authorize a client for GitHub integration. + * Redirect the user to GitHub for authorization. + */ + private async oAuthAppAuthorization() { + const redirect_uri = environment.GAUZY_GITHUB_REDIRECT_URL; + const client_id = environment.GAUZY_GITHUB_CLIENT_ID; + + // Define your query parameters + const queryParams = toParams({ + 'redirect_uri': `${redirect_uri}`, + 'client_id': `${client_id}`, + 'scope': 'user' + }); + + // Construct the external URL with the query parameters + const externalUrl = `${GITHUB_AUTHORIZATION_URL}?${queryParams.toString()}`; + + /** Navigate to the external URL with query parameters */ + window.location.replace(externalUrl); + } + + /** + * Handle GitHub App installation flow. + */ + private async githubAppInstallation() { + if (!this.organization) { + return; + } + try { + if (!this.window || this.window.closed) { + // If no window is open or the existing window is closed, open a new one + this.openPopupWindow(); + } else { + // If a window is already open, you can handle it here (e.g., focus or bring it to the front) + this.window.focus(); + } + this.checkPopupWindowStatus(); + } catch (error) { + console.log('Error while setup GitHub integration: %s', error?.message); + } + } + + /** + * Open a popup window for GitHub App installation. + */ + private async openPopupWindow() { + const { id: organizationId, tenantId } = this.organization; + const state = `${organizationId}|${tenantId}`; + + const width = 600, height = 600; + const left = window.innerWidth - width; // Adjust the left position to place it on the right side + const top = window.innerHeight / 2 - height / 2; + + // Specify a unique window name to identify the window + const windowName = 'githubAppInstallationWindow'; + + // Check if a window with the same name is already open + if (window.frames[windowName] && !window.frames[windowName].closed) { + // A window with the same name is already open, so focus on it + window.frames[windowName].focus(); + } else { + /** Navigate to the target external URL */ + const url = `https://github.com/apps/${environment.GAUZY_GITHUB_APP_NAME}/installations/new?state=${state.toString()}`; + + /** Navigate to the external URL with query parameters */ + this.window = window.open( + url, + windowName, + `width=${width}, + height=${height}, + top=${top}, + left=${left}, + toolbar=no, + location=no, + status=no, + menubar=no, + scrollbars=yes, + resizable=yes, + `); + } + } + + /** + * Check the status of the popup window. + */ + private async checkPopupWindowStatus() { + const timer = setInterval(() => { + console.log(this.window); + if (this.window == null || this.window.closed) { + clearInterval(timer); // Stop checking when the window is closed + /** */ + this.handleClosedPopupWindow(); + } else { + console.log('popup window still open'); + } + }, 1000); // Check every second (adjust the interval as needed) + } + + /** + * Handle the case when the popup window is closed. + * + * @param ms + */ + private handleClosedPopupWindow(ms: number = 1000) { + this.isLoading = false; + console.log('popup window closed'); + + // Delay navigation by 5 seconds before redirecting + setTimeout(() => { + this._router.navigate(['/pages/integrations/github']); + }, ms); // 5000 milliseconds = 5 seconds + } +} diff --git a/apps/gauzy/src/app/pages/integrations/github/github-routing.module.ts b/apps/gauzy/src/app/pages/integrations/github/github-routing.module.ts new file mode 100644 index 00000000000..bab42e03c32 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/github-routing.module.ts @@ -0,0 +1,42 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { IntegrationEnum } from '@gauzy/contracts'; +import { IntegrationResolver } from './../integration.resolver'; +import { GithubWizardComponent } from './components/wizard/wizard.component'; +import { GithubInstallationComponent } from './components/installation/installation.component'; +import { GithubComponent } from './github.component'; +import { GithubViewComponent } from './components/view/view.component'; + +const routes: Routes = [ + { + path: '', + component: GithubComponent, + data: { + integration: IntegrationEnum.GITHUB + }, + resolve: { + integration: IntegrationResolver + }, + runGuardsAndResolvers: 'always', + children: [ + { + path: ':integrationId', + component: GithubViewComponent, + }, + { + path: 'setup/wizard', + component: GithubWizardComponent, + } + ] + }, + { + path: 'setup/installation', + component: GithubInstallationComponent + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class GithubRoutingModule { } diff --git a/apps/gauzy/src/app/pages/integrations/github/github.component.ts b/apps/gauzy/src/app/pages/integrations/github/github.component.ts new file mode 100644 index 00000000000..757d4d79219 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/github.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Data, Router } from '@angular/router'; +import { tap } from 'rxjs/operators'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + template: ` + + ` +}) +export class GithubComponent implements OnInit { + + constructor( + private readonly _router: Router, + private readonly _activatedRoute: ActivatedRoute, + ) { } + + /** + * + */ + ngOnInit() { + this._activatedRoute.data + .pipe( + tap(({ integration }: Data) => { + if (integration) { + this._router.navigate(['/pages/integrations/github', integration.id]); + } else { + this._router.navigate(['/pages/integrations/github/setup/wizard']); + } + }), + untilDestroyed(this) + ) + .subscribe(); + } +} diff --git a/apps/gauzy/src/app/pages/integrations/github/github.config.ts b/apps/gauzy/src/app/pages/integrations/github/github.config.ts new file mode 100644 index 00000000000..c0918ed44d0 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/github.config.ts @@ -0,0 +1 @@ +export const GITHUB_AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize'; diff --git a/apps/gauzy/src/app/pages/integrations/github/github.module.ts b/apps/gauzy/src/app/pages/integrations/github/github.module.ts new file mode 100644 index 00000000000..1b3a3c1f89b --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/github/github.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NbCardModule, NbSpinnerModule } from '@nebular/theme'; +import { Ng2SmartTableModule } from 'ng2-smart-table'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { TranslateModule } from './../../../@shared/translate/translate.module'; +import { GithubRoutingModule } from './github-routing.module'; +import { GithubComponent } from './github.component'; +import { GithubWizardComponent } from './components/wizard/wizard.component'; +import { GithubInstallationComponent } from './components/installation/installation.component'; +import { GithubViewComponent } from './components/view/view.component'; + +@NgModule({ + declarations: [ + GithubComponent, + GithubWizardComponent, + GithubInstallationComponent, + GithubViewComponent + ], + imports: [ + CommonModule, + NbCardModule, + NbSpinnerModule, + Ng2SmartTableModule, + NgSelectModule, + GithubRoutingModule, + TranslateModule + ] +}) +export class GithubModule { } diff --git a/apps/gauzy/src/app/pages/integrations/integration.resolver.ts b/apps/gauzy/src/app/pages/integrations/integration.resolver.ts new file mode 100644 index 00000000000..f6c5e32bfb0 --- /dev/null +++ b/apps/gauzy/src/app/pages/integrations/integration.resolver.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { catchError, Observable, of } from 'rxjs'; +import { IIntegrationTenant } from '@gauzy/contracts'; +import { IntegrationsService, Store } from './../../@core/services'; + +@Injectable({ + providedIn: 'root' +}) +export class IntegrationResolver implements Resolve> { + constructor( + private readonly _integrationsService: IntegrationsService, + private readonly _store: Store, + ) { } + + /** + * + * @param route + * @returns + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const integration = route.data['integration']; + const { id: organizationId, tenantId } = this._store.selectedOrganization; + + return this._integrationsService.checkRememberState({ + organizationId, + tenantId, + name: integration + }).pipe( + catchError(error => of(error)) + ); + } +} diff --git a/apps/gauzy/src/app/pages/integrations/integrations-routing.module.ts b/apps/gauzy/src/app/pages/integrations/integrations-routing.module.ts index 41ae814ebb5..96a8bb48c56 100644 --- a/apps/gauzy/src/app/pages/integrations/integrations-routing.module.ts +++ b/apps/gauzy/src/app/pages/integrations/integrations-routing.module.ts @@ -4,9 +4,10 @@ import { IntegrationsComponent } from './integrations.component'; const routes: Routes = [ { - path: '', + path: 'new', component: IntegrationsComponent }, + /** Integrations List */ { path: 'upwork', loadChildren: () => import('../upwork/upwork.module').then( @@ -24,6 +25,12 @@ const routes: Routes = [ loadChildren: () => import('./gauzy-ai/gauzy-ai.module').then( (m) => m.GauzyAIModule ) + }, + { + path: 'github', + loadChildren: () => import('./github/github.module').then( + (m) => m.GithubModule + ) } ]; diff --git a/apps/gauzy/src/app/pages/integrations/integrations.component.html b/apps/gauzy/src/app/pages/integrations/integrations.component.html index d11e48e1d62..371e1ef39ae 100644 --- a/apps/gauzy/src/app/pages/integrations/integrations.component.html +++ b/apps/gauzy/src/app/pages/integrations/integrations.component.html @@ -88,18 +88,18 @@

{{ 'INTEGRATIONS.AVAILABLE_INTEGRATIONS' | translate }}

image not found -
- +
+ {{ 'INTEGRATIONS.COMING_SOON' | translate }}
diff --git a/apps/gauzy/src/app/pages/integrations/integrations.component.scss b/apps/gauzy/src/app/pages/integrations/integrations.component.scss index 6a5023adbee..dd0174d7ed2 100644 --- a/apps/gauzy/src/app/pages/integrations/integrations.component.scss +++ b/apps/gauzy/src/app/pages/integrations/integrations.component.scss @@ -69,7 +69,7 @@ img { width: 100%; } -.comingSoon-wrapper { +.coming-soon-wrapper { position: absolute; right: -5px; top: -5px; @@ -78,7 +78,7 @@ img { overflow: hidden; } -.comingSoon { +.coming-soon { position: absolute; width: 130px; transform: rotate(45deg); @@ -190,4 +190,4 @@ nb-option { button { box-shadow: 0 1px 1px 0 rgb(0 0 0 / 15%); } -} \ No newline at end of file +} diff --git a/apps/gauzy/src/app/pages/integrations/integrations.component.ts b/apps/gauzy/src/app/pages/integrations/integrations.component.ts index f9ab77bc9b2..2378d53de3f 100644 --- a/apps/gauzy/src/app/pages/integrations/integrations.component.ts +++ b/apps/gauzy/src/app/pages/integrations/integrations.component.ts @@ -8,7 +8,7 @@ import { import { Observable } from 'rxjs'; import { IIntegrationViewModel, - DEFAULT_INTEGRATION_PAID_FILTERS + IntegrationFilterEnum } from '@gauzy/contracts'; import { InitialFilter, @@ -29,7 +29,20 @@ export class IntegrationsComponent implements OnInit { isLoading$: Observable = this._integrationsStore.isLoading$; @ViewChild('searchElement', { static: true }) searchElement: ElementRef; - filters = DEFAULT_INTEGRATION_PAID_FILTERS; + public filters = [ + { + label: IntegrationFilterEnum.ALL, + value: 'all' + }, + { + label: IntegrationFilterEnum.FREE, + value: 'false' + }, + { + label: IntegrationFilterEnum.PAID, + value: 'true' + } + ]; constructor( private readonly _integrationsStore: IntegrationsStoreService, diff --git a/apps/gauzy/src/app/pages/pages-routing.module.ts b/apps/gauzy/src/app/pages/pages-routing.module.ts index 3a9a6b2c16d..48933309793 100644 --- a/apps/gauzy/src/app/pages/pages-routing.module.ts +++ b/apps/gauzy/src/app/pages/pages-routing.module.ts @@ -690,8 +690,8 @@ const routes: Routes = [ project: false, team: false, employee: false, - organization: false, - date: false + date: false, + organization: true, } } }, diff --git a/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.html b/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.html index 0d517f7db39..d2b62122d42 100644 --- a/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.html +++ b/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.html @@ -222,6 +222,13 @@ " >
+
+ + +
diff --git a/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.ts b/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.ts index c6f4122e23b..815978a0044 100644 --- a/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.ts +++ b/apps/gauzy/src/app/pages/projects/projects-mutation/projects-mutation.component.ts @@ -4,7 +4,6 @@ import { IEmployee, IOrganization, IOrganizationContact, - IOrganizationProject, ProjectBillingEnum, ITag, ProjectOwnerEnum, @@ -12,7 +11,9 @@ import { ContactType, ICurrency, OrganizationProjectBudgetTypeEnum, - IImageAsset + IImageAsset, + IOrganizationTeam, + IOrganizationProject, } from '@gauzy/contracts'; import { TranslateService } from '@ngx-translate/core'; import { Router } from '@angular/router'; @@ -39,11 +40,14 @@ import { ckEditorConfig } from "../../../@shared/ckeditor.config"; export class ProjectsMutationComponent extends TranslationBaseComponent implements OnInit { + @Input() teams: IOrganizationTeam[] = []; + FormHelpers: typeof FormHelpers = FormHelpers; OrganizationProjectBudgetTypeEnum = OrganizationProjectBudgetTypeEnum; TaskListTypeEnum = TaskListTypeEnum; members: string[] = []; selectedEmployeeIds: string[] = []; + selectedTeamIds: string[] = []; billings: string[] = Object.values(ProjectBillingEnum); owners: ProjectOwnerEnum[] = Object.values(ProjectOwnerEnum); taskViewModeTypes: TaskListTypeEnum[] = Object.values(TaskListTypeEnum); @@ -58,10 +62,11 @@ export class ProjectsMutationComponent extends TranslationBaseComponent */ public form: FormGroup = ProjectsMutationComponent.buildForm(this.fb); static buildForm(fb: FormBuilder): FormGroup { - return fb.group({ + const form = fb.group({ imageUrl: [], imageId: [], tags: [], + teams: [], public: [], billable: [], name: ['', Validators.required], @@ -91,6 +96,8 @@ export class ProjectsMutationComponent extends TranslationBaseComponent CompareDateValidator.validateDate('startDate', 'endDate') ] }); + form.get('teams').setValue([]); + return form; } /* @@ -187,6 +194,7 @@ export class ProjectsMutationComponent extends TranslationBaseComponent this.selectedEmployeeIds = project.members.map( (member: IEmployee) => member.id ); + this.members = this.selectedEmployeeIds; this.form.patchValue({ imageUrl: project.imageUrl || null, @@ -210,6 +218,8 @@ export class ProjectsMutationComponent extends TranslationBaseComponent openSource: project.openSource || null, projectUrl: project.projectUrl || null, openSourceProjectUrl: project.openSourceProjectUrl || null, + teams: this.project.teams.map((team: IOrganizationTeam) => team.id), + }); this.form.updateValueAndValidity(); } @@ -245,6 +255,11 @@ export class ProjectsMutationComponent extends TranslationBaseComponent this.members = members; } + onTeamsSelected(teams: IOrganizationTeam[]) { + this.form.get('teams').setValue(teams); + this.form.get('teams').updateValueAndValidity(); + } + cancel() { this.canceled.emit(); } @@ -283,7 +298,7 @@ export class ProjectsMutationComponent extends TranslationBaseComponent startDate: startDate, endDate: endDate, members: this.members.map((id) => this.employees.find((e) => e.id === id)).filter((e) => !!e), - + teams: this.form.get('teams').value.map((id) => this.teams.find((e) => e.id === id)).filter((e) => !!e), // Description Step description: description, tags: tags || [], diff --git a/apps/gauzy/src/app/pages/projects/projects.component.html b/apps/gauzy/src/app/pages/projects/projects.component.html index fa02817eb92..de44b25f25f 100644 --- a/apps/gauzy/src/app/pages/projects/projects.component.html +++ b/apps/gauzy/src/app/pages/projects/projects.component.html @@ -129,6 +129,7 @@

= this.subject$; + teams$: Subject = new Subject(); + private _refresh$: Subject = new Subject(); projectsTable: Ng2SmartTableComponent; @@ -87,6 +93,7 @@ export class ProjectsComponent extends PaginationFilterBaseComponent implements constructor( private readonly organizationContactService: OrganizationContactService, private readonly organizationProjectsService: OrganizationProjectsService, + private readonly organizationTeamService: OrganizationTeamsService, private readonly toastrService: ToastrService, private readonly store: Store, public readonly translateService: TranslateService, @@ -109,6 +116,14 @@ export class ProjectsComponent extends PaginationFilterBaseComponent implements untilDestroyed(this) ) .subscribe(); + + this.teams$.pipe( + debounceTime(300), + tap(() => this._refresh$.next(true)), + tap(() => this.loadOrganizationTeams()), + untilDestroyed(this) + ).subscribe(); + const storeOrganization$ = this.store.selectedOrganization$; const storeEmployee$ = this.store.selectedEmployee$; combineLatest([storeOrganization$, storeEmployee$]) @@ -121,6 +136,7 @@ export class ProjectsComponent extends PaginationFilterBaseComponent implements }), tap(() => this._refresh$.next(true)), tap(() => this.project$.next(true)), + tap(() => this.teams$.next(true)), untilDestroyed(this) ) .subscribe(); @@ -267,7 +283,7 @@ export class ProjectsComponent extends PaginationFilterBaseComponent implements this.smartTableSource = new ServerDataSource(this.httpClient, { endPoint: `${API_PREFIX}/organization-projects/pagination`, - relations: ['organizationContact', 'organization', 'members', 'members.user', 'tags'], + relations: ['organizationContact', 'organization', 'members', 'members.user', 'tags', 'teams'], join: { alias: 'organization_project', leftJoin: { @@ -323,6 +339,31 @@ export class ProjectsComponent extends PaginationFilterBaseComponent implements } } + /** + * load organization projects + * + * @returns + */ + private async loadOrganizationTeams(): Promise { + if (!this.organization || !this.store.hasAnyPermission( + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ORG_TEAM_VIEW + )) { + return; + } + + const { tenantId } = this.store.user; + const { id: organizationId } = this.organization; + + this.teams = (await this.organizationTeamService.getAll( + [], + { + organizationId, + tenantId + } + )).items; + } + private async loadGridLayoutData() { if (this._isGridCardLayout) { await this.smartTableSource.getElements(); diff --git a/apps/gauzy/src/app/pages/projects/projects.module.ts b/apps/gauzy/src/app/pages/projects/projects.module.ts index 5834f76326d..b46894e0a20 100644 --- a/apps/gauzy/src/app/pages/projects/projects.module.ts +++ b/apps/gauzy/src/app/pages/projects/projects.module.ts @@ -43,6 +43,7 @@ import { HeaderTitleModule } from '../../@shared/components/header-title/header- import { GauzyButtonActionModule } from '../../@shared/gauzy-button-action/gauzy-button-action.module'; import { PaginationModule } from '../../@shared/pagination/pagination.module'; import { CardGridModule } from '../../@shared/card-grid/card-grid.module'; +import { TeamSelectModule } from '../../@shared/team-select/team-select.module'; @NgModule({ imports: [ @@ -85,9 +86,10 @@ import { CardGridModule } from '../../@shared/card-grid/card-grid.module'; GauzyButtonActionModule, NbTooltipModule, PaginationModule, + TeamSelectModule, CardGridModule ], declarations: [ProjectsComponent, ProjectsMutationComponent], providers: [OrganizationProjectsService, OrganizationContactService] }) -export class ProjectsModule {} +export class ProjectsModule { } diff --git a/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.html b/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.html index d93ddb9d4a6..87c20036ce0 100644 --- a/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.html +++ b/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.html @@ -1,7 +1,7 @@
{{ @@ -18,10 +18,10 @@
@@ -33,12 +33,12 @@
'CONTEXT_MENU.PROJECT' | translate }}

@@ -48,9 +48,11 @@
'TASKS_PAGE.TASKS_STATUS' | translate }} @@ -59,8 +61,8 @@
@@ -76,11 +78,11 @@
@@ -90,9 +92,11 @@
{{ 'TASKS_PAGE.TASK_PRIORITY' | translate }} @@ -102,18 +106,18 @@
{{ 'TASKS_PAGE.TASK_SIZE' | translate }}
@@ -122,19 +126,19 @@
-
@@ -146,47 +150,47 @@
}}
@@ -197,8 +201,11 @@
- - +
@@ -206,18 +213,18 @@
diff --git a/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.ts b/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.ts index 78d4cd3bffc..c150ecb5eae 100644 --- a/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.ts +++ b/apps/gauzy/src/app/pages/tasks/components/my-task-dialog/my-task-dialog.component.ts @@ -1,12 +1,12 @@ import { Component, Input, OnInit } from '@angular/core'; import { - ITask, - IOrganizationProject, IEmployee, + IOrganizationProject, ITag, + ITask, TaskStatusEnum, } from '@gauzy/contracts'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NbDialogRef } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; import * as moment from 'moment'; @@ -22,7 +22,6 @@ import { import { CKEditor4 } from 'ckeditor4-angular/ckeditor'; import { richTextCKEditorConfig } from 'apps/gauzy/src/app/@shared/ckeditor.config'; - const initialTaskValue = { title: '', project: null, @@ -40,10 +39,10 @@ const initialTaskValue = { templateUrl: './my-task-dialog.component.html', styleUrls: ['./my-task-dialog.component.scss'], }) -export class MyTaskDialogComponent extends TranslationBaseComponent - implements OnInit { - - form: FormGroup; +export class MyTaskDialogComponent + extends TranslationBaseComponent + implements OnInit +{ selectedTaskId: string; projects: IOrganizationProject[]; employees: IEmployee[] = []; @@ -56,6 +55,7 @@ export class MyTaskDialogComponent extends TranslationBaseComponent tags: ITag[] = []; @Input() task: Partial = {}; public ckConfig: CKEditor4.Config = richTextCKEditorConfig; + public form: FormGroup = MyTaskDialogComponent.buildForm(this.fb); constructor( public readonly dialogRef: NbDialogRef, @@ -71,13 +71,27 @@ export class MyTaskDialogComponent extends TranslationBaseComponent super(translateService); } - ngOnInit() { - this.ckConfig.editorplaceholder = this.translateService.instant('FORM.PLACEHOLDERS.DESCRIPTION'); - this.loadProjects(); - this.loadEmployees(); - this.initializeForm( - Object.assign({}, initialTaskValue, this.selectedTask || this.task) - ); + static buildForm(fb: FormBuilder): FormGroup { + return fb.group({ + number: [{ value: '', disabled: true }], + title: [null, Validators.required], + project: [], + projectId: [], + status: [TaskStatusEnum.OPEN], + priority: [], + size: [], + members: [], + estimateDays: [], + estimateHours: [null, [Validators.min(0), Validators.max(23)]], + estimateMinutes: [null, [Validators.min(0), Validators.max(59)]], + dueDate: [], + description: [], + tags: [], + teams: [], + taskStatus: [], + taskSize: [], + taskPriority: [], + }); } private async loadProjects() { @@ -93,6 +107,32 @@ export class MyTaskDialogComponent extends TranslationBaseComponent if (items) this.projects = items; } + private async loadEmployees() { + const organizationId = this._organizationsStore.selectedOrganization.id; + if (!organizationId) { + return; + } + + const { items } = await firstValueFrom( + this.employeesService.getAll(['user'], { + organization: { id: organizationId }, + }) + ); + + this.employees = items; + } + + async ngOnInit() { + this.ckConfig.editorplaceholder = this.translateService.instant( + 'FORM.PLACEHOLDERS.DESCRIPTION' + ); + await this.loadProjects(); + await this.loadEmployees(); + this.initializeForm( + Object.assign({}, initialTaskValue, this.selectedTask || this.task) + ); + } + initializeForm({ title, description, @@ -103,7 +143,10 @@ export class MyTaskDialogComponent extends TranslationBaseComponent dueDate, tags, priority, - size + size, + taskStatus, + taskSize, + taskPriority, }: ITask) { const duration = moment.duration(estimate, 'seconds'); // select members from database of default value @@ -117,27 +160,23 @@ export class MyTaskDialogComponent extends TranslationBaseComponent if (members === null) { this.selectedMembers = [this.employeeId]; } - this.form = this.fb.group({ - number: [{ value: '', disabled: true }], - title: [title, Validators.required], - project: [project], + this.form.patchValue({ + title, + project, projectId: project ? project.id : null, - status: [status ? status : TaskStatusEnum.OPEN], - priority: [priority ? priority : null], - size: [size ? size : null], - members: [members], - estimateDays: [duration.days() || ''], - estimateHours: [ - duration.hours() || '', - [Validators.min(0), Validators.max(23)], - ], - estimateMinutes: [ - duration.minutes() || '', - [Validators.min(0), Validators.max(59)], - ], - dueDate: [dueDate], - description: [description], - tags: [tags], + status, + priority, + size, + estimateDays: duration.days(), + estimateHours: duration.hours(), + estimateMinutes: duration.minutes(), + dueDate: dueDate ? new Date(dueDate) : null, + description, + tags, + members: [this.selectedMembers], + taskStatus, + taskSize, + taskPriority, teams: [], }); this.tags = this.form.get('tags').value || []; @@ -163,6 +202,15 @@ export class MyTaskDialogComponent extends TranslationBaseComponent onSave() { if (this.form.valid) { + this.form + .get('status') + .setValue(this.form.get('taskStatus').value?.name); + this.form + .get('priority') + .setValue(this.form.get('taskPriority').value?.name); + this.form + .get('size') + .setValue(this.form.get('taskSize').value?.name); this.form .get('members') .setValue( @@ -181,19 +229,4 @@ export class MyTaskDialogComponent extends TranslationBaseComponent selectedProject(project: IOrganizationProject) { this.form.patchValue({ project }); } - - private async loadEmployees() { - const organizationId = this._organizationsStore.selectedOrganization.id; - if (!organizationId) { - return; - } - - const { items } = await firstValueFrom( - this.employeesService.getAll(['user'], { - organization: { id: organizationId }, - }) - ); - - this.employees = items; - } } diff --git a/apps/gauzy/src/app/pages/tasks/components/task/task.component.ts b/apps/gauzy/src/app/pages/tasks/components/task/task.component.ts index 27ac2fbfbed..9ab425af5ef 100644 --- a/apps/gauzy/src/app/pages/tasks/components/task/task.component.ts +++ b/apps/gauzy/src/app/pages/tasks/components/task/task.component.ts @@ -1,19 +1,19 @@ -import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; -import { Router, ActivatedRoute } from '@angular/router'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { combineLatest, firstValueFrom, Observable, Subject } from 'rxjs'; -import { first, tap, filter, debounceTime } from 'rxjs/operators'; +import { debounceTime, filter, first, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { Ng2SmartTableComponent } from 'ng2-smart-table'; import { NbDialogService } from '@nebular/theme'; import { - ITask, - IOrganizationProject, ComponentLayoutStyleEnum, - TaskListTypeEnum, IOrganization, + IOrganizationProject, ISelectedEmployee, + ITask, PermissionsEnum, + TaskListTypeEnum, } from '@gauzy/contracts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { distinctUntilChange } from '@gauzy/common-angular'; @@ -61,6 +61,8 @@ export class TaskComponent extends PaginationFilterBaseComponent implements OnInit, OnDestroy { + private _refresh$: Subject = new Subject(); + private _tasks: ITask[] = []; settingsSmartTable: object; loading: boolean = false; disableButton: boolean = true; @@ -81,16 +83,7 @@ export class TaskComponent selectedEmployee: ISelectedEmployee; selectedEmployeeId: ISelectedEmployee['id']; selectedProject: IOrganizationProject; - private _refresh$: Subject = new Subject(); - private _tasks: ITask[] = []; - tasksTable: Ng2SmartTableComponent; - @ViewChild('tasksTable') set content(content: Ng2SmartTableComponent) { - if (content) { - this.tasksTable = content; - this.onChangedSource(); - } - } constructor( private readonly dialogService: NbDialogService, @@ -110,6 +103,205 @@ export class TaskComponent this.setView(); } + @ViewChild('tasksTable') set content(content: Ng2SmartTableComponent) { + if (content) { + this.tasksTable = content; + this.onChangedSource(); + } + } + + /** + * If, default project is selected from header + * + * @returns + */ + get isDefaultProject() { + if (this.selectedProject) { + return this.selectedProject.id === this.defaultProject.id; + } + return true; + } + + /** + * return store instance as per page + */ + get storeInstance() { + if (this.isTasksPage()) { + return this._taskStore; + } else if (this.isMyTasksPage()) { + return this._myTaskStore; + } else if (this.isTeamTaskPage()) { + return this._teamTaskStore; + } + } + + private initTasks(): void { + const path = this._activatedRoute.snapshot.url[0].path; + if (path === 'me') { + this.viewComponentName = ComponentEnum.MY_TASKS; + this.availableTasks$ = this.myTasks$; + return; + } + if (path === 'team') { + this.viewComponentName = ComponentEnum.TEAM_TASKS; + this.availableTasks$ = this.teamTasks$; + return; + } + this.viewComponentName = ComponentEnum.ALL_TASKS; + this.availableTasks$ = this.tasks$; + return; + } + + private _applyTranslationOnSmartTable() { + this.translateService.onLangChange + .pipe( + tap(() => this._loadSmartTableSettings()), + untilDestroyed(this) + ) + .subscribe(); + } + + private _loadSmartTableSettings() { + const pagination: IPaginationBase = this.getPagination(); + this.settingsSmartTable = { + actions: false, + pager: { + display: false, + perPage: pagination ? pagination.itemsPerPage : 10, + }, + noDataMessage: this.getTranslation('SM_TABLE.NO_DATA.TASK'), + columns: { + taskNumber: { + title: this.getTranslation('TASKS_PAGE.TASK_ID'), + type: 'string', + width: '10%', + filter: { + type: 'custom', + component: InputFilterComponent, + }, + filterFunction: (prefix: string) => { + this.setFilter({ field: 'prefix', search: prefix }); + }, + }, + description: { + title: this.getTranslation('TASKS_PAGE.TASKS_TITLE'), + type: 'custom', + class: 'align-row', + renderComponent: NotesWithTagsComponent, + filter: { + type: 'custom', + component: InputFilterComponent, + }, + filterFunction: (value) => { + this.setFilter({ field: 'title', search: value }); + }, + }, + project: { + title: this.getTranslation('TASKS_PAGE.TASKS_PROJECT'), + type: 'custom', + renderComponent: ProjectComponent, + filter: false, + }, + createdAt: { + title: this.getTranslation('SM_TABLE.CREATED_AT'), + type: 'custom', + filter: false, + renderComponent: CreatedAtComponent, + }, + creator: { + title: this.getTranslation('TASKS_PAGE.TASKS_CREATOR'), + type: 'custom', + renderComponent: CreateByComponent, + filter: { + type: 'custom', + component: InputFilterComponent, + }, + filterFunction: (value: string) => { + this.setFilter({ + field: 'creator.firstName', + search: value, + }); + }, + }, + ...this.getColumnsByPage(), + dueDate: { + title: this.getTranslation('TASKS_PAGE.DUE_DATE'), + type: 'custom', + filter: { + type: 'custom', + component: InputFilterComponent, + }, + filterFunction: (value) => { + this.setFilter({ field: 'dueDate', search: value }); + }, + renderComponent: DateViewComponent, + }, + status: { + title: this.getTranslation('TASKS_PAGE.TASKS_STATUS'), + type: 'custom', + width: '10%', + renderComponent: StatusViewComponent, + filter: { + type: 'custom', + component: TaskStatusFilterComponent, + }, + filterFunction: (value) => { + this.setFilter({ + field: 'status', + search: value?.name, + }); + }, + }, + }, + }; + } + + private getColumnsByPage() { + if (this.isTasksPage()) { + return { + employeesMergedTeams: { + title: + this.getTranslation('TASKS_PAGE.TASK_MEMBERS') + + '/' + + this.getTranslation('TASKS_PAGE.TASK_TEAMS'), + type: 'custom', + filter: false, + renderComponent: EmployeesMergedTeamsComponent, + }, + }; + } else if (this.isMyTasksPage()) { + return { + assignTo: { + title: this.getTranslation('TASKS_PAGE.TASK_ASSIGNED_TO'), + type: 'custom', + filter: false, + renderComponent: AssignedToComponent, + }, + }; + } else if (this.isTeamTaskPage()) { + return { + assignTo: { + title: this.getTranslation('TASKS_PAGE.TASK_ASSIGNED_TO'), + type: 'custom', + width: '12%', + renderComponent: AssignedToComponent, + filter: { + type: 'custom', + component: OrganizationTeamFilterComponent, + }, + filterFunction: (value) => { + this.setFilter({ + field: 'teams', + search: value ? [value.id] : [], + }); + }, + }, + }; + } else { + return {}; + } + } + ngOnInit() { this._loadSmartTableSettings(); this._applyTranslationOnSmartTable(); @@ -174,23 +366,6 @@ export class TaskComponent .subscribe(); } - private initTasks(): void { - const path = this._activatedRoute.snapshot.url[0].path; - if (path === 'me') { - this.viewComponentName = ComponentEnum.MY_TASKS; - this.availableTasks$ = this.myTasks$; - return; - } - if (path === 'team') { - this.viewComponentName = ComponentEnum.TEAM_TASKS; - this.availableTasks$ = this.teamTasks$; - return; - } - this.viewComponentName = ComponentEnum.ALL_TASKS; - this.availableTasks$ = this.tasks$; - return; - } - setView() { this._store .componentLayout$(this.viewComponentName) @@ -224,15 +399,6 @@ export class TaskComponent .subscribe(); } - private _applyTranslationOnSmartTable() { - this.translateService.onLangChange - .pipe( - tap(() => this._loadSmartTableSettings()), - untilDestroyed(this) - ) - .subscribe(); - } - /* * Register Smart Table Source Config */ @@ -247,13 +413,13 @@ export class TaskComponent const { id: organizationId } = this.organization; this.smartTableSource = new ServerDataSource(this.httpClient, { - ...(this.viewComponentName == ComponentEnum.ALL_TASKS + ...(this.viewComponentName === ComponentEnum.ALL_TASKS ? { endPoint: `${API_PREFIX}/tasks/pagination` } : {}), - ...(this.viewComponentName == ComponentEnum.TEAM_TASKS + ...(this.viewComponentName === ComponentEnum.TEAM_TASKS ? { endPoint: `${API_PREFIX}/tasks/team` } : {}), - ...(this.viewComponentName == ComponentEnum.MY_TASKS + ...(this.viewComponentName === ComponentEnum.MY_TASKS ? { endPoint: `${API_PREFIX}/tasks/me` } : {}), relations: [ @@ -265,6 +431,9 @@ export class TaskComponent 'teams.members.employee.user', 'creator', 'organizationSprint', + 'taskStatus', + 'taskSize', + 'taskPriority', ], join: { alias: 'task', @@ -341,144 +510,6 @@ export class TaskComponent } } - private _loadSmartTableSettings() { - const pagination: IPaginationBase = this.getPagination(); - this.settingsSmartTable = { - actions: false, - pager: { - display: false, - perPage: pagination ? pagination.itemsPerPage : 10, - }, - noDataMessage: this.getTranslation('SM_TABLE.NO_DATA.TASK'), - columns: { - taskNumber: { - title: this.getTranslation('TASKS_PAGE.TASK_ID'), - type: 'string', - width: '10%', - filter: { - type: 'custom', - component: InputFilterComponent, - }, - filterFunction: (prefix: string) => { - this.setFilter({ field: 'prefix', search: prefix }); - }, - }, - description: { - title: this.getTranslation('TASKS_PAGE.TASKS_TITLE'), - type: 'custom', - class: 'align-row', - renderComponent: NotesWithTagsComponent, - filter: { - type: 'custom', - component: InputFilterComponent, - }, - filterFunction: (value) => { - this.setFilter({ field: 'title', search: value }); - }, - }, - project: { - title: this.getTranslation('TASKS_PAGE.TASKS_PROJECT'), - type: 'custom', - renderComponent: ProjectComponent, - filter: false, - }, - createdAt: { - title: this.getTranslation('SM_TABLE.CREATED_AT'), - type: 'custom', - filter: false, - renderComponent: CreatedAtComponent, - }, - creator: { - title: this.getTranslation('TASKS_PAGE.TASKS_CREATOR'), - type: 'custom', - renderComponent: CreateByComponent, - filter: { - type: 'custom', - component: InputFilterComponent, - }, - filterFunction: (value: string) => { - this.setFilter({ - field: 'creator.firstName', - search: value, - }); - }, - }, - ...this.getColumnsByPage(), - dueDate: { - title: this.getTranslation('TASKS_PAGE.DUE_DATE'), - type: 'custom', - filter: { - type: 'custom', - component: InputFilterComponent, - }, - filterFunction: (value) => { - this.setFilter({ field: 'dueDate', search: value }); - }, - renderComponent: DateViewComponent, - }, - status: { - title: this.getTranslation('TASKS_PAGE.TASKS_STATUS'), - type: 'custom', - width: '10%', - renderComponent: StatusViewComponent, - filter: { - type: 'custom', - component: TaskStatusFilterComponent, - }, - filterFunction: (value) => { - this.setFilter({ field: 'status', search: value }); - }, - }, - }, - }; - } - - private getColumnsByPage() { - if (this.isTasksPage()) { - return { - employeesMergedTeams: { - title: - this.getTranslation('TASKS_PAGE.TASK_MEMBERS') + - '/' + - this.getTranslation('TASKS_PAGE.TASK_TEAMS'), - type: 'custom', - filter: false, - renderComponent: EmployeesMergedTeamsComponent, - }, - }; - } else if (this.isMyTasksPage()) { - return { - assignTo: { - title: this.getTranslation('TASKS_PAGE.TASK_ASSIGNED_TO'), - type: 'custom', - filter: false, - renderComponent: AssignedToComponent, - }, - }; - } else if (this.isTeamTaskPage()) { - return { - assignTo: { - title: this.getTranslation('TASKS_PAGE.TASK_ASSIGNED_TO'), - type: 'custom', - width: '12%', - renderComponent: AssignedToComponent, - filter: { - type: 'custom', - component: OrganizationTeamFilterComponent, - }, - filterFunction: (value) => { - this.setFilter({ - field: 'teams', - search: value ? [value.id] : [], - }); - }, - }, - }; - } else { - return {}; - } - } - async createTaskDialog() { let dialog; if (this.isTasksPage()) { @@ -708,31 +739,6 @@ export class TaskComponent }); } - /** - * If, default project is selected from header - * - * @returns - */ - get isDefaultProject() { - if (this.selectedProject) { - return this.selectedProject.id === this.defaultProject.id; - } - return true; - } - - /** - * return store instance as per page - */ - get storeInstance() { - if (this.isTasksPage()) { - return this._taskStore; - } else if (this.isMyTasksPage()) { - return this._myTaskStore; - } else if (this.isTeamTaskPage()) { - return this._teamTaskStore; - } - } - /* * Clear selected item */ diff --git a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.html b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.html index b7facd0d656..eb724b5b845 100644 --- a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.html +++ b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.html @@ -1,4 +1,4 @@ -
+
@@ -16,7 +16,7 @@ @@ -24,13 +24,26 @@
+ +
+ + + +
+
diff --git a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.scss b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.scss index 7a678ddada7..55718e481e7 100644 --- a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.scss +++ b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.scss @@ -11,4 +11,18 @@ nb-card { .selected { background-color: var(--gauzy-sidebar-background-3); box-shadow: -6px 0 0 0 rgba(0 0 0 / 10%); -} \ No newline at end of file +} + +.view { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px; +} +.status-view { + cursor: pointer; +} + +::ng-deep nb-popover > span.arrow { + display: none; +} diff --git a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.ts b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.ts index 3c00a980875..3534ec52a82 100644 --- a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.ts +++ b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/task/task.component.ts @@ -1,29 +1,47 @@ import { + AfterViewInit, Component, - Input, EventEmitter, - Output, + Input, + OnDestroy, OnInit, - OnDestroy + Output, } from '@angular/core'; -import { ITask, TaskStatusEnum } from '@gauzy/contracts'; +import { + IOrganization, + IOrganizationProject, + IPagination, + ITask, + ITaskStatus, + ITaskStatusFindInput, + TaskStatusEnum, +} from '@gauzy/contracts'; import { NbMenuService } from '@nebular/theme'; -import { tap, filter, map, takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; +import { filter, map, takeUntil, tap } from 'rxjs/operators'; +import { combineLatest, debounceTime, Subject } from 'rxjs'; import { TranslationBaseComponent } from 'apps/gauzy/src/app/@shared/language-base/translation-base.component'; import { TranslateService } from '@ngx-translate/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { Store, TaskStatusesService } from '../../../../../../../@core'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { distinctUntilChange } from '@gauzy/common-angular'; +@UntilDestroy({ checkProperties: true }) @Component({ selector: 'ga-sprint-task', templateUrl: './task.component.html', - styleUrls: ['./task.component.scss'] + styleUrls: ['./task.component.scss'], }) export class SprintTaskComponent extends TranslationBaseComponent - implements OnInit, OnDestroy + implements OnInit, AfterViewInit, OnDestroy { + private onDestroy$ = new Subject(); + private subject$: Subject = new Subject(); + private organization: IOrganization; + private projectId: IOrganizationProject['id']; @Input() task: any; - @Input() isSelected: boolean = false; + @Input() isSelected = false; @Output() taskActionEvent: EventEmitter<{ action: string; task: ITask; @@ -32,25 +50,34 @@ export class SprintTaskComponent new EventEmitter(); taskStatusList: any; taskActions: any; - private onDestroy$ = new Subject(); + public statuses$: BehaviorSubject = new BehaviorSubject([]); constructor( private nbMenuService: NbMenuService, - readonly translate: TranslateService + readonly translate: TranslateService, + private readonly store: Store, + private readonly taskStatusesService: TaskStatusesService ) { super(translate); } ngOnInit(): void { + this.subject$ + .pipe( + debounceTime(200), + tap(() => this.getStatuses()), + untilDestroyed(this) + ) + .subscribe(); this.taskActions = [ { title: this.getTranslation('TASKS_PAGE.EDIT_TASK'), - action: 'EDIT_TASK' + action: 'EDIT_TASK', }, { title: this.getTranslation('TASKS_PAGE.DELETE_TASK'), - action: 'DELETE_TASK' - } + action: 'DELETE_TASK', + }, ]; this.taskStatusList = this.getStatusList(this.task.status); @@ -68,13 +95,13 @@ export class SprintTaskComponent this.changeStatusEvent.emit({ status: item.title, id: this.task.id, - title: this.task.title + title: this.task.title, }); break; case 'updateTask': this.taskActionEvent.emit({ action: item.action, - task: this.task + task: this.task, }); } }), @@ -83,6 +110,23 @@ export class SprintTaskComponent .subscribe(); } + ngAfterViewInit(): void { + const storeOrganization$ = this.store.selectedOrganization$; + const storeProject$ = this.store.selectedProject$; + combineLatest([storeOrganization$, storeProject$]) + .pipe( + distinctUntilChange(), + filter(([organization]) => !!organization), + tap(([organization, project]) => { + this.organization = organization; + this.projectId = project ? project.id : null; + }), + tap(() => this.subject$.next(true)), + untilDestroyed(this) + ) + .subscribe(); + } + getStatusList(filterOption: string): { title: TaskStatusEnum }[] { return Object.values(TaskStatusEnum) .filter((status) => status !== filterOption) @@ -97,6 +141,48 @@ export class SprintTaskComponent this.changeStatusEvent.emit(evt); } + getStatuses() { + if (!this.organization) { + return; + } + + const { tenantId } = this.store.user; + const { id: organizationId } = this.organization; + + this.taskStatusesService + .get({ + tenantId, + organizationId, + ...(this.projectId + ? { + projectId: this.projectId, + } + : {}), + }) + .pipe( + map(({ items, total }: IPagination) => + total > 0 ? items : this.taskStatusList + ), + tap((statuses: ITaskStatus[]) => this.statuses$.next(statuses)), + untilDestroyed(this) + ) + .subscribe(); + } + + public updateStatus(taskStatus: ITaskStatus) { + this.changeStatusEvent.emit({ + status: taskStatus.name as TaskStatusEnum, + id: this.task.id, + title: this.task.title, + taskStatus, + }); + this.task = { + ...this.task, + taskStatus, + status: taskStatus.name as TaskStatusEnum, + }; + } + ngOnDestroy() { this.onDestroy$.next(); this.onDestroy$.complete(); diff --git a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/tasks-sprint-view.component.ts b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/tasks-sprint-view.component.ts index 6177e310df3..86c116c2c57 100644 --- a/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/tasks-sprint-view.component.ts +++ b/apps/gauzy/src/app/pages/tasks/components/task/tasks-layouts/tasks-sprint-view/tasks-sprint-view.component.ts @@ -5,20 +5,20 @@ import { Output, EventEmitter, SimpleChanges, - OnChanges + OnChanges, } from '@angular/core'; import { ITask, IOrganizationSprint, IOrganizationProject, - IOrganization + IOrganization, } from '@gauzy/contracts'; import { Observable } from 'rxjs'; import { map, tap, filter, take } from 'rxjs/operators'; import { CdkDragDrop, moveItemInArray, - transferArrayItem + transferArrayItem, } from '@angular/cdk/drag-drop'; import { NbDialogService } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; @@ -27,14 +27,14 @@ import { GauzyEditableGridComponent } from '../../../../../../@shared/components import { SprintStoreService, Store, - TasksStoreService + TasksStoreService, } from './../../../../../../@core/services'; @UntilDestroy({ checkProperties: true }) @Component({ selector: 'ga-tasks-sprint-view', templateUrl: './tasks-sprint-view.component.html', - styleUrls: ['./tasks-sprint-view.component.scss'] + styleUrls: ['./tasks-sprint-view.component.scss'], }) export class TasksSprintViewComponent extends GauzyEditableGridComponent @@ -61,7 +61,7 @@ export class TasksSprintViewComponent tap((sprints: IOrganizationSprint[]) => { this.sprintIds = [ ...sprints.map((sprint: IOrganizationSprint) => sprint.id), - 'backlog' + 'backlog', ]; }) ); @@ -87,7 +87,7 @@ export class TasksSprintViewComponent this.backlogTasks = this.tasks; this.sprintActions = [ { title: this.getTranslation('TASKS_PAGE.EDIT_SPRINT') }, - { title: this.getTranslation('TASKS_PAGE.DELETE_SPRINT') } + { title: this.getTranslation('TASKS_PAGE.DELETE_SPRINT') }, ]; } @@ -102,7 +102,7 @@ export class TasksSprintViewComponent this.store$.fetchSprints({ organizationId, projectId: project.id, - tenantId + tenantId, }); }), untilDestroyed(this) @@ -121,7 +121,7 @@ export class TasksSprintViewComponent ) => { acc[sprint.id] = { ...sprint, - tasks: sprint.tasks || [] + tasks: sprint.tasks || [], }; return acc; }, @@ -169,7 +169,7 @@ export class TasksSprintViewComponent this.selectedTask = task === this.selectedTask ? null : task; this.selectedTaskEvent.emit({ data: task, - isSelected: this.isSelected + isSelected: this.isSelected, }); } @@ -188,7 +188,7 @@ export class TasksSprintViewComponent organizationSprint: this.sprints.find( (sprint) => sprint.id === event.container.id - ) || null + ) || null, }) .pipe(untilDestroyed(this)) .subscribe(); @@ -213,13 +213,14 @@ export class TasksSprintViewComponent } } - changeTaskStatus({ id, status, title }: Partial): void { + changeTaskStatus({ id, taskStatus, status, title }: Partial): void { this.taskStore .editTask({ id, status, title, - organizationId: this.organizationId + organizationId: this.organizationId, + taskStatus, }) .pipe(untilDestroyed(this)) .subscribe(); @@ -230,7 +231,7 @@ export class TasksSprintViewComponent this.store$ .updateSprint({ ...sprint, - isActive: false + isActive: false, }) .pipe(take(1), untilDestroyed(this)) .subscribe(); diff --git a/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.html b/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.html index f1f6cc1c8c1..151d419e4f1 100644 --- a/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.html +++ b/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.html @@ -1,7 +1,7 @@
- +
{{ @@ -18,10 +18,10 @@
@@ -33,12 +33,12 @@
'CONTEXT_MENU.PROJECT' | translate }}
@@ -48,9 +48,11 @@
'TASKS_PAGE.TASKS_STATUS' | translate }} @@ -62,11 +64,11 @@
'TASKS_PAGE.TASK_TEAMS' | translate }} @@ -104,9 +106,11 @@
{{ 'TASKS_PAGE.TASK_PRIORITY' | translate }} @@ -116,18 +120,18 @@
{{ 'TASKS_PAGE.TASK_SIZE' | translate }}
@@ -136,19 +140,19 @@
-
@@ -160,47 +164,47 @@
}}
@@ -211,7 +215,11 @@
- +
@@ -219,18 +227,18 @@
diff --git a/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.ts b/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.ts index 0fb1a8db45b..d139536915c 100644 --- a/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.ts +++ b/apps/gauzy/src/app/pages/tasks/components/team-task-dialog/team-task-dialog.component.ts @@ -1,13 +1,13 @@ import { Component, Input, OnInit } from '@angular/core'; import { - ITask, - IOrganizationProject, IEmployee, + IOrganizationProject, IOrganizationTeam, ITag, + ITask, TaskStatusEnum, } from '@gauzy/contracts'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NbDialogRef } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; import * as moment from 'moment'; @@ -20,7 +20,7 @@ import { ToastrService, } from '../../../../@core/services'; import { CKEditor4 } from 'ckeditor4-angular/ckeditor'; -import { richTextCKEditorConfig } from "../../../../@shared/ckeditor.config"; +import { richTextCKEditorConfig } from '../../../../@shared/ckeditor.config'; const initialTaskValue = { title: '', @@ -32,6 +32,9 @@ const initialTaskValue = { dueDate: null, description: '', tags: null, + taskStatus: null, + taskSize: null, + taskPriority: null, }; @Component({ @@ -39,10 +42,10 @@ const initialTaskValue = { templateUrl: './team-task-dialog.component.html', styleUrls: ['./team-task-dialog.component.scss'], }) -export class TeamTaskDialogComponent extends TranslationBaseComponent - implements OnInit { - - form: FormGroup; +export class TeamTaskDialogComponent + extends TranslationBaseComponent + implements OnInit +{ selectedTaskId: string; projects: IOrganizationProject[]; employees: IEmployee[] = []; @@ -56,6 +59,8 @@ export class TeamTaskDialogComponent extends TranslationBaseComponent public ckConfig: CKEditor4.Config = richTextCKEditorConfig; @Input() task: Partial = {}; + public form: FormGroup = TeamTaskDialogComponent.buildForm(this.fb); + constructor( public readonly dialogRef: NbDialogRef, private readonly fb: FormBuilder, @@ -70,16 +75,27 @@ export class TeamTaskDialogComponent extends TranslationBaseComponent super(translateService); } - ngOnInit() { - this.ckConfig.editorplaceholder = this.translateService.instant('FORM.PLACEHOLDERS.DESCRIPTION'); - this.tenantId = this.store.user.tenantId; - this.organizationId = this._organizationsStore.selectedOrganization.id; - - this.loadProjects(); - this.loadTeams(); - this.initializeForm( - Object.assign({}, initialTaskValue, this.selectedTask || this.task) - ); + static buildForm(fb: FormBuilder): FormGroup { + return fb.group({ + number: [{ value: '', disabled: true }], + title: [null, Validators.required], + project: [], + projectId: [], + status: [TaskStatusEnum.OPEN], + priority: [], + size: [], + members: [], + estimateDays: [], + estimateHours: [null, [Validators.min(0), Validators.max(23)]], + estimateMinutes: [null, [Validators.min(0), Validators.max(59)]], + dueDate: [], + description: [], + tags: [], + teams: [], + taskStatus: [], + taskSize: [], + taskPriority: [], + }); } private async loadProjects() { @@ -92,6 +108,20 @@ export class TeamTaskDialogComponent extends TranslationBaseComponent if (items) this.projects = items; } + async ngOnInit() { + this.ckConfig.editorplaceholder = this.translateService.instant( + 'FORM.PLACEHOLDERS.DESCRIPTION' + ); + this.tenantId = this.store.user.tenantId; + this.organizationId = this._organizationsStore.selectedOrganization.id; + + await this.loadProjects(); + await this.loadTeams(); + this.initializeForm( + Object.assign({}, initialTaskValue, this.selectedTask || this.task) + ); + } + initializeForm({ title, description, @@ -103,7 +133,10 @@ export class TeamTaskDialogComponent extends TranslationBaseComponent dueDate, tags, priority, - size + size, + taskStatus, + taskSize, + taskPriority, }: ITask) { const duration = moment.duration(estimate, 'seconds'); this.selectedTeams = (teams || []).map((team) => team.id); @@ -116,30 +149,25 @@ export class TeamTaskDialogComponent extends TranslationBaseComponent // if (teams === null) { // this.selectedTeams = [this.employeeId]; // } - this.form = this.fb.group({ - number: [{ value: '', disabled: true }], - title: [title, Validators.required], - project: [project], + this.form.patchValue({ + title, + project, projectId: project ? project.id : null, - status: [status ? status : TaskStatusEnum.OPEN], - priority: [priority ? priority : null], - size: [size ? size : null], - members: [members], - estimateDays: [duration.days() || ''], - estimateHours: [ - duration.hours() || '', - [Validators.min(0), Validators.max(23)], - ], - estimateMinutes: [ - duration.minutes() || '', - [Validators.min(0), Validators.max(59)], - ], - dueDate: [dueDate], - description: [description], - tags: [tags], - teams: [this.selectedTeams], + status, + priority, + size, + estimateDays: duration.days(), + estimateHours: duration.hours(), + estimateMinutes: duration.minutes(), + dueDate: dueDate ? new Date(dueDate) : null, + members, + description, + tags, + teams: this.selectedTeams, + taskStatus, + taskSize, + taskPriority, }); - this.tags = this.form.get('tags').value || []; } @@ -169,6 +197,15 @@ export class TeamTaskDialogComponent extends TranslationBaseComponent .map((id) => this.teams.find((e) => e.id === id)) .filter((e) => !!e) ); + this.form + .get('status') + .setValue(this.form.get('taskStatus').value?.name); + this.form + .get('priority') + .setValue(this.form.get('taskPriority').value?.name); + this.form + .get('size') + .setValue(this.form.get('taskSize').value?.name); this.dialogRef.close(this.form.value); } } diff --git a/apps/gauzy/src/app/pages/tasks/tasks.module.ts b/apps/gauzy/src/app/pages/tasks/tasks.module.ts index 3038ed67bc8..2e326ee9b31 100644 --- a/apps/gauzy/src/app/pages/tasks/tasks.module.ts +++ b/apps/gauzy/src/app/pages/tasks/tasks.module.ts @@ -21,7 +21,8 @@ import { NbTabsetModule, NbActionsModule, NbContextMenuModule, - NbTooltipModule + NbTooltipModule, + NbPopoverModule, } from '@nebular/theme'; import { NgxPermissionsModule } from 'ngx-permissions'; import { ThemeModule } from '../../@theme/theme.module'; @@ -62,7 +63,7 @@ import { CKEditorModule } from 'ckeditor4-angular'; TaskSettingsComponent, ProjectViewComponent, TasksSprintViewComponent, - SprintTaskComponent + SprintTaskComponent, ], imports: [ NbTooltipModule, @@ -110,7 +111,8 @@ import { CKEditorModule } from 'ckeditor4-angular'; TaskNumberFieldModule, NgxPermissionsModule.forChild(), DirectivesModule, - CKEditorModule - ] + CKEditorModule, + NbPopoverModule, + ], }) -export class TasksModule { } +export class TasksModule {} diff --git a/apps/gauzy/src/app/pages/teams/teams-mutation/teams-mutation.component.html b/apps/gauzy/src/app/pages/teams/teams-mutation/teams-mutation.component.html index 6f522fffbbb..fc00c118dc9 100644 --- a/apps/gauzy/src/app/pages/teams/teams-mutation/teams-mutation.component.html +++ b/apps/gauzy/src/app/pages/teams/teams-mutation/teams-mutation.component.html @@ -61,6 +61,11 @@
+ +
+
diff --git a/apps/gauzy/src/app/pages/teams/teams.component.ts b/apps/gauzy/src/app/pages/teams/teams.component.ts index d1e155316bb..8f298279a54 100644 --- a/apps/gauzy/src/app/pages/teams/teams.component.ts +++ b/apps/gauzy/src/app/pages/teams/teams.component.ts @@ -16,6 +16,8 @@ import { RolesEnum, ComponentLayoutStyleEnum, ISelectedEmployee, + IOrganizationProject, + PermissionsEnum, } from '@gauzy/contracts'; import { NbDialogRef, NbDialogService } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; @@ -39,6 +41,7 @@ import { } from '../../@shared/table-components'; import { EmployeesService, + OrganizationProjectsService, OrganizationTeamsService, Store, ToastrService @@ -65,6 +68,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent teams: IOrganizationTeam[] = []; employees: IEmployee[] = []; tags: ITag[] = []; + projects: IOrganizationProject[] = []; viewComponentName: ComponentEnum; dataLayoutStyle = ComponentLayoutStyleEnum.TABLE; @@ -77,6 +81,8 @@ export class TeamsComponent extends PaginationFilterBaseComponent public organization: IOrganization; teams$: Subject = this.subject$; employees$: Subject = new Subject(); + projects$: Subject = new Subject(); + selected = { team: null, state: false @@ -94,6 +100,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent constructor( private readonly organizationTeamsService: OrganizationTeamsService, private readonly employeesService: EmployeesService, + private readonly projectService: OrganizationProjectsService, private readonly toastrService: ToastrService, private readonly dialogService: NbDialogService, private readonly store: Store, @@ -134,6 +141,15 @@ export class TeamsComponent extends PaginationFilterBaseComponent ) .subscribe(); + this.projects$ + .pipe( + debounceTime(300), + tap(() => this._refresh$.next(true)), + tap(() => this.loadOrganizationProjects()), + untilDestroyed(this) + ) + .subscribe(); + const storeOrganization$ = this.store.selectedOrganization$; const storeEmployee$ = this.store.selectedEmployee$; combineLatest([storeOrganization$, storeEmployee$]) @@ -147,6 +163,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent tap(() => this._refresh$.next(true)), tap(() => this.teams$.next(true)), tap(() => this.employees$.next(true)), + tap(() => this.projects$.next(true)), untilDestroyed(this) ) .subscribe(); @@ -158,6 +175,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent ), tap(() => this.teams$.next(true)), tap(() => this.employees$.next(true)), + tap(() => this.projects$.next(true)), debounceTime(1000), tap(() => this.openDialog(this.addEditTemplate, false)), untilDestroyed(this) @@ -315,6 +333,31 @@ export class TeamsComponent extends PaginationFilterBaseComponent this.employees = items; } + /** + * load organization projects + * + * @returns + */ + private async loadOrganizationProjects(): Promise { + if (!this.organization || !this.store.hasAnyPermission( + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.ORG_PROJECT_VIEW + )) { + return; + } + + const { tenantId } = this.store.user; + const { id: organizationId } = this.organization; + + this.projects = (await this.projectService.getAll( + [], + { + organizationId, + tenantId + } + )).items; + } + public getTagsByEmployeeId(id: string) { const employee = this.employees.find((empl) => empl.id === id); return employee ? employee.tags : []; @@ -340,6 +383,7 @@ export class TeamsComponent extends PaginationFilterBaseComponent 'members.role', 'members.employee.user', 'tags', + 'projects' ], where: { organizationId, diff --git a/apps/gauzy/src/app/pages/teams/teams.module.ts b/apps/gauzy/src/app/pages/teams/teams.module.ts index 60f03a25a20..bb64e4fb3f4 100644 --- a/apps/gauzy/src/app/pages/teams/teams.module.ts +++ b/apps/gauzy/src/app/pages/teams/teams.module.ts @@ -29,6 +29,7 @@ import { EmployeeMultiSelectModule } from '../../@shared/employee/employee-multi import { GauzyButtonActionModule } from '../../@shared/gauzy-button-action/gauzy-button-action.module'; import { PaginationModule } from '../../@shared/pagination/pagination.module'; import { ImageUploaderModule } from '../../@shared/image-uploader/image-uploader.module'; +import { ProjectSelectModule } from '../../@shared/project-select/project-select.module'; @NgModule({ imports: [ @@ -54,6 +55,7 @@ import { ImageUploaderModule } from '../../@shared/image-uploader/image-uploader TranslateModule, HeaderTitleModule, EmployeeMultiSelectModule, + ProjectSelectModule, PaginationModule, GauzyButtonActionModule, CommonModule, diff --git a/apps/gauzy/src/assets/i18n/bg.json b/apps/gauzy/src/assets/i18n/bg.json index aee4c0fe900..c1281b25a7a 100644 --- a/apps/gauzy/src/assets/i18n/bg.json +++ b/apps/gauzy/src/assets/i18n/bg.json @@ -521,6 +521,8 @@ "REMOVE_IMAGE": "Премахни снимка", "UPLOADER_PLACEHOLDER": "Снимка", "UPLOADER_DOCUMENT_PLACEHOLDER": "URL", + "ADD_REMOVE_PROJECTS": "Добавяне или премахване на проекти", + "ADD_REMOVE_TEAMS": "Добавяне или премахване на екипи", "ADD_REMOVE_EMPLOYEES": "Добави или премахни служители", "ADD_REMOVE_CANDIDATE": "Add Candidate", "ADD_REMOVE_EMPLOYEE": "Add Interviewer", diff --git a/apps/gauzy/src/assets/i18n/en.json b/apps/gauzy/src/assets/i18n/en.json index 424f79f6488..c052e2a39f3 100644 --- a/apps/gauzy/src/assets/i18n/en.json +++ b/apps/gauzy/src/assets/i18n/en.json @@ -292,7 +292,8 @@ "REJECTED_DATE": "Rejected Date", "TIME_TRACKING": "Time Tracking", "CREATED_AT": "Created At", - "SCREEN_CAPTURE": "Screen Capture" + "SCREEN_CAPTURE": "Screen Capture", + "NUMBER": "Number" }, "FORM": { "USERNAME": "Username", @@ -525,6 +526,8 @@ "UPLOADER_PLACEHOLDER": "Image", "UPLOADER_DOCUMENT_PLACEHOLDER": "URL", "ADD_REMOVE_EMPLOYEES": "Add or Remove Employees", + "ADD_REMOVE_PROJECTS": "Add or Remove Projects", + "ADD_REMOVE_TEAMS": "Add or Remove Teams", "ADD_REMOVE_CANDIDATE": "Add Candidate", "ADD_REMOVE_EMPLOYEE": "Add Interviewer", "ADD_REMOVE_INTERVIEWER": "Select interviewer", @@ -1210,6 +1213,10 @@ "API_KEY": "Gauzy AI key", "API_SECRET": "Gauzy AI Secret" }, + "GITHUB_PAGE": { + "NAME": "GitHub", + "SELECT_REPOSITORY": "Select Repository" + }, "COMING_SOON": "Coming soon", "RE_INTEGRATE": "Re-integrate", "SETTINGS": "Settings", diff --git a/apps/gauzy/src/assets/i18n/es.json b/apps/gauzy/src/assets/i18n/es.json index 4be361a0698..392c3a8edb8 100644 --- a/apps/gauzy/src/assets/i18n/es.json +++ b/apps/gauzy/src/assets/i18n/es.json @@ -525,6 +525,8 @@ "UPLOADER_PLACEHOLDER": "Imagen", "UPLOADER_DOCUMENT_PLACEHOLDER": "URL (Uniform Resource Locator)", "ADD_REMOVE_EMPLOYEES": "Agregar o eliminar empleados.", + "ADD_REMOVE_PROJECTS": "Agregar o quitar proyecto", + "ADD_REMOVE_TEAMS": "Agregar o eliminar equipos", "ADD_REMOVE_CANDIDATE": "Agregar candidato", "ADD_REMOVE_EMPLOYEE": "Añadir entrevistador", "ADD_REMOVE_INTERVIEWER": "Seleccionar entrevistador", diff --git a/apps/gauzy/src/assets/i18n/fr.json b/apps/gauzy/src/assets/i18n/fr.json index 117a9bb05e7..f71755d32bf 100644 --- a/apps/gauzy/src/assets/i18n/fr.json +++ b/apps/gauzy/src/assets/i18n/fr.json @@ -525,6 +525,7 @@ "UPLOADER_PLACEHOLDER": "Image", "UPLOADER_DOCUMENT_PLACEHOLDER": "URL (Uniform Resource Locator)", "ADD_REMOVE_EMPLOYEES": "Ajouter ou Supprimer des Employés", + "ADD_REMOVE_PROJECTS": "Ajouter ou supprimer un projet", "ADD_REMOVE_CANDIDATE": "Ajouter un candidat", "ADD_REMOVE_EMPLOYEE": "Ajouter un intervieweur", "ADD_REMOVE_INTERVIEWER": "Sélectionnez l'intervieweur", diff --git a/apps/gauzy/src/assets/i18n/he.json b/apps/gauzy/src/assets/i18n/he.json index e564be9804d..3ca66742093 100644 --- a/apps/gauzy/src/assets/i18n/he.json +++ b/apps/gauzy/src/assets/i18n/he.json @@ -522,6 +522,8 @@ "UPLOADER_PLACEHOLDER": "תמונה", "UPLOADER_DOCUMENT_PLACEHOLDER": "URL", "ADD_REMOVE_EMPLOYEES": "Add or Remove Employees", + "ADD_REMOVE_PROJECTS": "הוסף או הסר פרויקט", + "ADD_REMOVE_TEAMS": "הוסף או הסר צוותים", "ADD_REMOVE_CANDIDATE": "Add Candidate", "ADD_REMOVE_EMPLOYEE": "Add Interviewer", "ADD_REMOVE_INTERVIEWER": "Select interviewer", diff --git a/apps/gauzy/src/assets/i18n/ru.json b/apps/gauzy/src/assets/i18n/ru.json index ed6695d0876..dbdf9731122 100644 --- a/apps/gauzy/src/assets/i18n/ru.json +++ b/apps/gauzy/src/assets/i18n/ru.json @@ -522,6 +522,8 @@ "UPLOADER_PLACEHOLDER": "Изображение", "UPLOADER_DOCUMENT_PLACEHOLDER": "URL", "ADD_REMOVE_EMPLOYEES": "Добавление или удаление сотрудников", + "ADD_REMOVE_PROJECTS": "Добавить или удалить проект", + "ADD_REMOVE_TEAMS": "Добавить или удалить команды", "ADD_REMOVE_CANDIDATE": "Добавить Кандидата", "ADD_REMOVE_EMPLOYEE": "Добавить интервьюера", "ADD_REMOVE_INTERVIEWER": "Выбрать интервьюера", diff --git a/apps/gauzy/src/environments/model.ts b/apps/gauzy/src/environments/model.ts index e452b073c42..50f426c11be 100644 --- a/apps/gauzy/src/environments/model.ts +++ b/apps/gauzy/src/environments/model.ts @@ -69,4 +69,10 @@ export interface Environment { GAUZY_CLOUD_APP: string; FILE_PROVIDER: string; + + /** Github Integration */ + GAUZY_GITHUB_APP_NAME: string; + GAUZY_GITHUB_APP_ID: string; + GAUZY_GITHUB_CLIENT_ID: string; + GAUZY_GITHUB_REDIRECT_URL: string; } diff --git a/apps/server/src/package.json b/apps/server/src/package.json index 9f4e5fe4f44..78a1abe80e4 100755 --- a/apps/server/src/package.json +++ b/apps/server/src/package.json @@ -29,11 +29,13 @@ "../../../packages/plugins/knowledge-base", "../../../packages/plugins/changelog", "../../../packages/common", - "../../../packages/plugins/integration-ai", "../../../packages/config", "../../../packages/contracts", + "../../../packages/plugins/integration-ai", "../../../packages/plugins/integration-hubstaff", "../../../packages/plugins/integration-upwork", + "../../../packages/plugins/integration-github", + "../../../packages/plugins/integration-jira", "../../../packages/plugin" ] }, diff --git a/nx.json b/nx.json index 0a27992f015..5234e6fe88f 100644 --- a/nx.json +++ b/nx.json @@ -16,7 +16,7 @@ }, "tasksRunnerOptions": { "default": { - "runner": "@nrwl/workspace/tasks-runners/default", + "runner": "nx-cloud", "options": { "cacheableOperations": [ "build", @@ -24,7 +24,8 @@ "test", "e2e" ], - "parallel": 1 + "parallel": 1, + "accessToken": "NWJlMDFmMTMtYzUyZS00ZGE4LTk0NzctNWIzZTkxMzNhMTFlfHJlYWQtd3JpdGU=" } } }, diff --git a/package.json b/package.json index 129a02c42fc..38678208723 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "doc:build-serve": "compodoc -p tsconfig.json -d docs -s", "postinstall.electron": "yarn electron-builder install-app-deps && yarn node tools/electron/postinstall", "postinstall.web": "yarn node ./decorate-angular-cli.js && yarn npx ngcc --properties es2016 browser module main --first-only --create-ivy-entry-points && yarn node tools/web/postinstall", - "build:desktop": "cross-env NODE_ENV=production yarn run copy-files-i18n-desktop && yarn run postinstall.electron && yarn build:package:all && yarn run config:prod && yarn run config:desktop:prod && yarn ng:prod build desktop --prod --base-href ./ && yarn run prepare:desktop && yarn ng:prod build desktop-api --prod && yarn ng:prod build api-desktop --prod && yarn ng:prod build desktop-ui --prod --base-href ./ && yarn run copy-files-desktop && yarn run copy-assets-gauzy", + "build:desktop": "cross-env NODE_ENV=production yarn run copy-files-i18n-desktop && yarn run postinstall.electron && yarn build:package:all && yarn run config:prod && yarn run config:desktop:prod && yarn ng:prod build desktop --prod --base-href ./ && yarn run prepare:desktop && yarn ng:prod build desktop-api --prod && yarn ng:prod build api-desktop --prod --output-path=dist/apps/desktop/desktop-api && yarn ng:prod build desktop-ui --prod --base-href ./ && yarn run copy-files-desktop && yarn run copy-assets-gauzy", "build:desktop:local": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=7000 yarn run config:prod && yarn run build:desktop && electron dist/apps/desktop", "build:desktop:linux": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=7000 yarn run config:prod && yarn run build:desktop && npm config set cache .cache && yarn electron-builder -c.electronVersion=20.2.0 build --linux --project dist/apps/desktop", "build:desktop:windows": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=7000 yarn run config:prod && yarn run build:desktop && npm config set cache .cache && yarn electron-builder -c.electronVersion=20.2.0 -c.extraMetadata.author.name=Ever build --windows --project dist/apps/desktop", @@ -124,10 +124,12 @@ "build:package:contracts": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/contracts build", "build:package:config": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/config build", "build:package:plugin": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugin build", - "build:package:plugins": "yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:product-reviews", + "build:package:plugins": "yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:integration-jira", "build:package:plugin:integration-ai": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-ai build", "build:package:plugin:integration-hubstaff": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-hubstaff build", "build:package:plugin:integration-upwork": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-upwork build", + "build:package:plugin:integration-github": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-github build", + "build:package:plugin:integration-jira": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/integration-jira build", "build:package:plugin:product-reviews": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/product-reviews build", "build:package:plugin:knowledge-base": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/knowledge-base build", "build:package:plugin:changelog": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/plugins/changelog build", @@ -139,7 +141,7 @@ "build:package:gauzy": "yarn run build:package:contracts && yarn run build:package:common && yarn run build:package:common-angular && yarn run build:package:config && yarn run build:package:plugin && yarn run build:package:plugins && yarn run build:package:auth && yarn run build:package:core && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog", "build:package:desktop-ui-lib": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=7000 yarn --cwd ./packages/desktop-ui-lib build", "copy-files-desktop": "copyfiles -f packages/core/src/**/*.gql dist/apps/desktop/data/", - "build:desktop-timer": "cross-env NODE_ENV=production yarn copy-files-i18n-desktop-timer && yarn run postinstall.electron && yarn build:package:all && yarn run config:prod && yarn run config:desktop-timer:prod && yarn ng build desktop-timer --prod --base-href ./ && yarn run prepare:desktop-timer && yarn ng build api-desktop --prod --output-path=dist/apps/desktop-timer/desktop-api && yarn run copy-assets-gauzy-timer", + "build:desktop-timer": "cross-env NODE_ENV=production yarn copy-files-i18n-desktop-timer && yarn run postinstall.electron && yarn build:package:all && yarn run config:prod && yarn run config:desktop-timer:prod && yarn ng:prod build desktop-timer --prod --base-href ./ && yarn run prepare:desktop-timer && yarn ng:prod build api-desktop --prod --output-path=dist/apps/desktop-timer/desktop-api && yarn run copy-assets-gauzy-timer", "prepare:desktop-timer": "yarn run postinstall.electron && tsc -p apps/desktop-timer/tsconfig.json", "build:desktop-timer:linux": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=7000 yarn run config:prod && yarn run build:desktop-timer && npm config set cache .cache && yarn electron-builder -c.electronVersion=20.2.0 build --linux --project dist/apps/desktop-timer", "build:desktop-timer:windows": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=7000 yarn run config:prod && yarn run build:desktop-timer && npm config set cache .cache && yarn electron-builder -c.electronVersion=20.2.0 -c.extraMetadata.author.name=Ever build --windows --project dist/apps/desktop-timer", @@ -276,6 +278,7 @@ "@nrwl/linter": "^13.8.0", "@nrwl/nest": "^13.8.0", "@nrwl/node": "^13.8.0", + "@nrwl/nx-cloud": "^16.3.0", "@nrwl/tao": "^13.8.0", "@nrwl/workspace": "^13.8.0", "@nstudio/angular": "^13.0.1", diff --git a/packages/auth/src/github/github.strategy.ts b/packages/auth/src/github/github.strategy.ts index d361086eb18..70c5a8af92f 100644 --- a/packages/auth/src/github/github.strategy.ts +++ b/packages/auth/src/github/github.strategy.ts @@ -30,17 +30,19 @@ export class GithubStrategy extends PassportStrategy(Strategy, 'github') { } } +/** + * + * @param configService + * @returns + */ export const config = (configService: ConfigService) => { - const GITHUB_CONFIG = configService.get( - 'githubConfig' - ) as IEnvironment['githubConfig']; + const github = configService.get('github') as IEnvironment['github']; const { baseUrl } = configService.apiConfigOptions as IApiServerOptions; return { - clientID: GITHUB_CONFIG.clientId || 'disabled', - clientSecret: GITHUB_CONFIG.clientSecret || 'disabled', - callbackURL: - GITHUB_CONFIG.callbackUrl || `${baseUrl}/api/auth/github/callback`, + clientID: github.clientId || 'disabled', + clientSecret: github.clientSecret || 'disabled', + callbackURL: github.callbackUrl || `${baseUrl}/api/auth/github/callback`, passReqToCallback: true, scope: ['user:email'] }; diff --git a/packages/common/src/interfaces/IGithubConfig.ts b/packages/common/src/interfaces/IGithubConfig.ts index 40f541e7398..6eb7b725f0f 100644 --- a/packages/common/src/interfaces/IGithubConfig.ts +++ b/packages/common/src/interfaces/IGithubConfig.ts @@ -1,5 +1,36 @@ -export interface IGithubConfig { +/** + * Configuration options for GitHub integration. + */ +export interface IGithubConfig extends Partial { + /** The GitHub OAuth App Client ID. */ readonly clientId: string; + + /** The GitHub OAuth App Client Secret. */ readonly clientSecret: string; - readonly callbackUrl?: string; + + /** The callback URL for GitHub OAuth authentication. */ + readonly callbackUrl: string; +} + +/** + * Configuration options for a GitHub Integration. + */ +export interface IGithubIntegrationConfig { + /** The GitHub App ID. */ + readonly appId: string; + + /** The name of the GitHub App. */ + readonly appName: string; + + /** The private key associated with the GitHub App. */ + readonly appPrivateKey: string; + + /** The URL to redirect to after GitHub App installation. */ + readonly postInstallUrl: string; + + /** The URL for receiving GitHub webhooks. */ + readonly webhookUrl: string; + + /** The secret used to secure GitHub webhooks. */ + readonly webhookSecret: string; } diff --git a/packages/common/src/interfaces/IHubstaffConfig.ts b/packages/common/src/interfaces/IHubstaffConfig.ts new file mode 100644 index 00000000000..925f722f7d9 --- /dev/null +++ b/packages/common/src/interfaces/IHubstaffConfig.ts @@ -0,0 +1,13 @@ +/** + * Configuration options for Hubstaff integration. + */ +export interface IHubstaffConfig { + /** The Hubstaff OAuth App Client ID. */ + readonly clientId: string; + + /** The Hubstaff OAuth App Client Secret. */ + readonly clientSecret: string; + + /** The URL to redirect to after Hubstaff App installation. */ + readonly postInstallUrl: string; +} diff --git a/packages/common/src/interfaces/IUpworkConfig.ts b/packages/common/src/interfaces/IUpworkConfig.ts index 6689e1f21e6..e4a9f8838c9 100644 --- a/packages/common/src/interfaces/IUpworkConfig.ts +++ b/packages/common/src/interfaces/IUpworkConfig.ts @@ -1,5 +1,16 @@ +/** + * Configuration options for Upwork integration. + */ export interface IUpworkConfig { - apiKey?: string; - apiSecret?: string; - callbackUrl: string; + /** The Upwork API Key. */ + readonly apiKey: string; + + /** The Upwork API Secret. */ + readonly apiSecret: string; + + /** The callback URL for Upwork OAuth authentication. */ + readonly callbackUrl: string; + + /** The URL to redirect to after Upwork App installation. */ + readonly postInstallUrl: string; } diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 39629d72af3..758058db10e 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -21,3 +21,4 @@ export * from './ITwitterConfig'; export * from './IUnleashConfig'; export * from './IUpworkConfig'; export * from './IWasabiConfig'; +export * from './IHubstaffConfig'; diff --git a/packages/config/src/config.service.ts b/packages/config/src/config.service.ts index c0600bddb6d..abfcc96f0fb 100644 --- a/packages/config/src/config.service.ts +++ b/packages/config/src/config.service.ts @@ -41,8 +41,8 @@ export class ConfigService { return this.config.assetOptions; } - get(key: keyof IEnvironment): IEnvironment[keyof IEnvironment] { - return this.environment[key]; + get(key: keyof IEnvironment): IEnvironment[keyof IEnvironment] { + return this.environment[key] as T; } isProd(): boolean { diff --git a/packages/config/src/database.ts b/packages/config/src/database.ts index cd125ddbe66..fe70da8a208 100644 --- a/packages/config/src/database.ts +++ b/packages/config/src/database.ts @@ -1,10 +1,11 @@ import * as path from 'path'; +import * as chalk from 'chalk'; import { TlsOptions } from 'tls'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { DataSourceOptions } from 'typeorm'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; -let dbType:string; +let dbType: string; if (process.env.DB_TYPE) dbType = process.env.DB_TYPE; @@ -13,6 +14,9 @@ else console.log(`Selected DB Type (DB_TYPE env var): ${dbType}`); +// `process` is a built-in global in Node.js, no need to `require()` +console.log(chalk.magenta(`Currently running Node.js version %s`), process.version); + let connectionConfig: TypeOrmModuleOptions; switch (dbType) { @@ -51,8 +55,8 @@ switch (dbType) { logger: 'file', // Removes console logging, instead logs all queries in a file ormlogs.log synchronize: process.env.DB_SYNCHRONIZE === 'true' ? true : false, // We are using migrations, synchronize should be set to false. uuidExtension: 'pgcrypto', - migrations: ["src/modules/not-exists/*.migration{.ts,.js}"], - entities: ["src/modules/not-exists/*.entity{.ts,.js}"], + migrations: ["src/modules/not-exists/*.migration{.ts,.js}"], + entities: ["src/modules/not-exists/*.entity{.ts,.js}"], }; connectionConfig = postgresConnectionOptions; diff --git a/packages/config/src/environments/environment.prod.ts b/packages/config/src/environments/environment.prod.ts index 0b5541a02ec..87c44de2afe 100644 --- a/packages/config/src/environments/environment.prod.ts +++ b/packages/config/src/environments/environment.prod.ts @@ -97,26 +97,33 @@ export const environment: IEnvironment = { clientId: process.env.FACEBOOK_CLIENT_ID, clientSecret: process.env.FACEBOOK_CLIENT_SECRET, fbGraphVersion: process.env.FACEBOOK_GRAPH_VERSION, - oauthRedirectUri: - process.env.FACEBOOK_CALLBACK_URL || - `${process.env.API_HOST}:${process.env.API_PORT}/api/auth/facebook/callback`, + oauthRedirectUri: process.env.FACEBOOK_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/facebook/callback`, state: '{fbstate}', }, googleConfig: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackUrl: - process.env.GOOGLE_CALLBACK_URL || - `${process.env.API_HOST}:${process.env.API_PORT}/api/auth/google/callback`, + callbackUrl: process.env.GOOGLE_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/google/callback`, }, - githubConfig: { - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackUrl: - process.env.GITHUB_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/google/callback`, + github: { + /**Github OAuth Configuration */ + clientId: process.env.GAUZY_GITHUB_CLIENT_ID, + clientSecret: process.env.GAUZY_GITHUB_CLIENT_SECRET, + callbackUrl: process.env.GAUZY_GITHUB_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/github/callback`, + + /** Github App Install Configuration */ + appId: process.env.GAUZY_GITHUB_APP_ID, + appName: process.env.GAUZY_GITHUB_APP_NAME, + appPrivateKey: process.env.GAUZY_GITHUB_APP_PRIVATE_KEY, + + /** Github App Post Install Configuration */ + postInstallUrl: process.env.GAUZY_GITHUB_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/github/setup/installation`, + + /** Github Webhook Configuration */ + webhookSecret: process.env.GAUZY_GITHUB_WEBHOOK_SECRET, + webhookUrl: process.env.GAUZY_GITHUB_WEBHOOK_URL || `${process.env.API_BASE_URL}/api/integration/github/webhook` }, microsoftConfig: { @@ -124,25 +131,19 @@ export const environment: IEnvironment = { clientSecret: process.env.MICROSOFT_CLIENT_SECRET, resource: process.env.MICROSOFT_RESOURCE, tenant: process.env.MICROSOFT_TENANT, - callbackUrl: - process.env.MICROSOFT_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/microsoft/callback`, + callbackUrl: process.env.MICROSOFT_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/microsoft/callback`, }, linkedinConfig: { clientId: process.env.LINKEDIN_CLIENT_ID, clientSecret: process.env.LINKEDIN_CLIENT_SECRET, - callbackUrl: - process.env.LINKEDIN_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/linked/callback`, + callbackUrl: process.env.LINKEDIN_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/linked/callback`, }, twitterConfig: { clientId: process.env.TWITTER_CLIENT_ID, clientSecret: process.env.TWITTER_CLIENT_SECRET, - callbackUrl: - process.env.TWITTER_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/twitter/callback`, + callbackUrl: process.env.TWITTER_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/twitter/callback`, }, fiverrConfig: { @@ -168,17 +169,26 @@ export const environment: IEnvironment = { dsn: process.env.SENTRY_DSN, }, - defaultIntegratedUserPass: - process.env.INTEGRATED_USER_DEFAULT_PASS || '123456', + defaultIntegratedUserPass: process.env.INTEGRATED_USER_DEFAULT_PASS || '123456', - upworkConfig: { + upwork: { + apiKey: process.env.UPWORK_API_KEY, + apiSecret: process.env.UPWORK_API_SECRET, callbackUrl: process.env.UPWORK_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/upwork/callback`, + postInstallUrl: process.env.UPWORK_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/upwork`, + }, + + hubstaff: { + /** Hubstaff Integration Configuration */ + clientId: process.env.HUBSTAFF_CLIENT_ID, + clientSecret: process.env.HUBSTAFF_CLIENT_SECRET, + /** Hubstaff Integration Post Install URL */ + postInstallUrl: process.env.HUBSTAFF_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/hubstaff`, }, isElectron: process.env.IS_ELECTRON === 'true' ? true : false, gauzyUserPath: process.env.GAUZY_USER_PATH, - allowSuperAdminRole: - process.env.ALLOW_SUPER_ADMIN_ROLE === 'false' ? false : true, + allowSuperAdminRole: process.env.ALLOW_SUPER_ADMIN_ROLE === 'false' ? false : true, /** * Endpoint for Gauzy AI API (optional), e.g.: http://localhost:3005/graphql diff --git a/packages/config/src/environments/environment.ts b/packages/config/src/environments/environment.ts index 5d06a35cd5c..d5fcc9e85b8 100644 --- a/packages/config/src/environments/environment.ts +++ b/packages/config/src/environments/environment.ts @@ -91,8 +91,7 @@ export const environment: IEnvironment = { api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, secure: process.env.CLOUDINARY_API_SECURE === 'false' ? false : true, - delivery_url: - process.env.CLOUDINARY_CDN_URL || `https://res.cloudinary.com`, + delivery_url: process.env.CLOUDINARY_CDN_URL || `https://res.cloudinary.com`, }, facebookConfig: { @@ -101,26 +100,33 @@ export const environment: IEnvironment = { clientId: process.env.FACEBOOK_CLIENT_ID, clientSecret: process.env.FACEBOOK_CLIENT_SECRET, fbGraphVersion: process.env.FACEBOOK_GRAPH_VERSION, - oauthRedirectUri: - process.env.FACEBOOK_CALLBACK_URL || - `${process.env.API_HOST}:${process.env.API_PORT}/api/auth/facebook/callback`, + oauthRedirectUri: process.env.FACEBOOK_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/facebook/callback`, state: '{fbstate}', }, googleConfig: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackUrl: - process.env.GOOGLE_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/google/callback`, + callbackUrl: process.env.GOOGLE_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/google/callback`, }, - githubConfig: { - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackUrl: - process.env.GITHUB_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/google/callback`, + github: { + /**Github OAuth Configuration */ + clientId: process.env.GAUZY_GITHUB_CLIENT_ID, + clientSecret: process.env.GAUZY_GITHUB_CLIENT_SECRET, + callbackUrl: process.env.GAUZY_GITHUB_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/github/callback`, + + /** Github App Install Configuration */ + appId: process.env.GAUZY_GITHUB_APP_ID, + appName: process.env.GAUZY_GITHUB_APP_NAME, + appPrivateKey: process.env.GAUZY_GITHUB_APP_PRIVATE_KEY, + + /** Github App Post Install Configuration */ + postInstallUrl: process.env.GAUZY_GITHUB_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/github/setup/installation`, + + /** Github Webhook Configuration */ + webhookSecret: process.env.GAUZY_GITHUB_WEBHOOK_SECRET, + webhookUrl: process.env.GAUZY_GITHUB_WEBHOOK_URL || `${process.env.API_BASE_URL}/api/integration/github/webhook` }, microsoftConfig: { @@ -128,25 +134,19 @@ export const environment: IEnvironment = { clientSecret: process.env.MICROSOFT_CLIENT_SECRET, resource: process.env.MICROSOFT_RESOURCE, tenant: process.env.MICROSOFT_TENANT, - callbackUrl: - process.env.MICROSOFT_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/microsoft/callback`, + callbackUrl: process.env.MICROSOFT_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/microsoft/callback`, }, linkedinConfig: { clientId: process.env.LINKEDIN_CLIENT_ID, clientSecret: process.env.LINKEDIN_CLIENT_SECRET, - callbackUrl: - process.env.LINKEDIN_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/linked/callback`, + callbackUrl: process.env.LINKEDIN_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/linked/callback`, }, twitterConfig: { clientId: process.env.TWITTER_CLIENT_ID, clientSecret: process.env.TWITTER_CLIENT_SECRET, - callbackUrl: - process.env.TWITTER_CALLBACK_URL || - `http://${process.env.API_HOST}:${process.env.API_PORT}/api/auth/twitter/callback`, + callbackUrl: process.env.TWITTER_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/twitter/callback`, }, fiverrConfig: { @@ -172,17 +172,26 @@ export const environment: IEnvironment = { dsn: process.env.SENTRY_DSN, }, - defaultIntegratedUserPass: - process.env.INTEGRATED_USER_DEFAULT_PASS || '123456', + defaultIntegratedUserPass: process.env.INTEGRATED_USER_DEFAULT_PASS || '123456', - upworkConfig: { + upwork: { + apiKey: process.env.UPWORK_API_KEY, + apiSecret: process.env.UPWORK_API_SECRET, callbackUrl: process.env.UPWORK_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/upwork/callback`, + postInstallUrl: process.env.UPWORK_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/upwork`, + }, + + hubstaff: { + /** Hubstaff Integration Configuration */ + clientId: process.env.HUBSTAFF_CLIENT_ID, + clientSecret: process.env.HUBSTAFF_CLIENT_SECRET, + /** Hubstaff Integration Post Install URL */ + postInstallUrl: process.env.HUBSTAFF_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/hubstaff`, }, isElectron: process.env.IS_ELECTRON === 'true' ? true : false, gauzyUserPath: process.env.GAUZY_USER_PATH, - allowSuperAdminRole: - process.env.ALLOW_SUPER_ADMIN_ROLE === 'false' ? false : true, + allowSuperAdminRole: process.env.ALLOW_SUPER_ADMIN_ROLE === 'false' ? false : true, /** * Endpoint for Gauzy AI API (optional), e.g.: http://localhost:3005/graphql diff --git a/packages/config/src/environments/ienvironment.ts b/packages/config/src/environments/ienvironment.ts index 1930be6c685..9ad255ad5ed 100644 --- a/packages/config/src/environments/ienvironment.ts +++ b/packages/config/src/environments/ienvironment.ts @@ -12,6 +12,7 @@ import { IFiverrConfig, IGithubConfig, IGoogleConfig, + IHubstaffConfig, IKeycloakConfig, ILinkedinConfig, IMicrosoftConfig, @@ -99,10 +100,9 @@ export interface IEnvironment { awsConfig?: IAwsConfig; wasabiConfig?: IWasabiConfig; cloudinaryConfig?: ICloudinaryConfig; - facebookConfig: IFacebookConfig; googleConfig: IGoogleConfig; - githubConfig: IGithubConfig; + github: IGithubConfig; /** Github Configuration */ microsoftConfig: IMicrosoftConfig; linkedinConfig: ILinkedinConfig; twitterConfig: ITwitterConfig; @@ -119,7 +119,10 @@ export interface IEnvironment { */ defaultIntegratedUserPass?: string; - upworkConfig?: IUpworkConfig; + /** Third Party Integrations */ + upwork?: IUpworkConfig; + hubstaff?: IHubstaffConfig; + isElectron?: boolean; gauzyUserPath?: string; allowSuperAdminRole?: boolean; diff --git a/packages/contracts/src/github.model.ts b/packages/contracts/src/github.model.ts new file mode 100644 index 00000000000..966db139fac --- /dev/null +++ b/packages/contracts/src/github.model.ts @@ -0,0 +1,37 @@ +import { IBasePerTenantAndOrganizationEntityModel } from "./base-entity.model"; + +export interface IGithubAppInstallInput extends IOAuthAppInstallInput { + installation_id?: string; + setup_action?: string; + state?: string; +} + +export interface IOAuthAppInstallInput extends IBasePerTenantAndOrganizationEntityModel { + provider?: string; + code?: string; +} + +/** */ +export interface IGithubRepository { + id: string; + node_id: string; + name: string; + full_name: string; + private: boolean; + [x: string]: any; +} + +export interface IGithubIssue { + id: string; + node_id: string; + number: string; + title: string; + state: string; + [x: string]: any; +} + +export interface IGithubRepositoryResponse { + total_count: number; + repository_selection: string; + repositories: IGithubRepository[] +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index c18d59575af..d3bc38a07c4 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -119,6 +119,7 @@ export * from './user.model'; export * from './wakatime.model'; export * from './organization-task-setting.model'; export * from './task-estimation.model'; +export * from './github.model'; export { IBaseEntityModel as BaseEntityModel } from './base-entity.model'; export { diff --git a/packages/contracts/src/integration.model.ts b/packages/contracts/src/integration.model.ts index c569276f331..20bd304511f 100644 --- a/packages/contracts/src/integration.model.ts +++ b/packages/contracts/src/integration.model.ts @@ -64,7 +64,8 @@ export interface IIntegration extends IBaseEntityModel { isPaid?: boolean; version?: string; docUrl?: string; - navigationUrl?: string; + provider?: string; + redirectUrl?: string; isFreeTrial?: boolean; freeTrialPeriod?: number; order?: number; @@ -75,8 +76,11 @@ export interface IIntegration extends IBaseEntityModel { export interface IIntegrationType extends IBaseEntityModel { name: string; + description: string; + icon: string; groupName: string; order: number; + integrations: IIntegration[]; } export interface IIntegrationFilter { @@ -142,12 +146,17 @@ export interface IIntegrationTenantCreateInput extends IBasePerTenantAndOrganiza settings?: IIntegrationSetting[]; } -export interface IIntegrationTenantUpdateInput extends Pick { } +export interface IIntegrationTenantUpdateInput extends Pick { + id?: IIntegrationTenant['id']; +} export enum IntegrationEnum { + IMPORT_EXPORT = 'Import_Export', UPWORK = 'Upwork', HUBSTAFF = 'Hubstaff', - GAUZY_AI = 'Gauzy AI' + GAUZY_AI = 'Gauzy_AI', + GITHUB = 'Github', + JIRA = 'Jira' } export enum IntegrationEntity { @@ -172,14 +181,16 @@ export enum IntegrationTypeGroupEnum { CATEGORIES = 'Categories' } -export enum IntegrationTypeNameEnum { +export enum IntegrationTypeEnum { ALL_INTEGRATIONS = 'All Integrations', FOR_SALES_TEAMS = 'For Sales Teams', FOR_ACCOUNTANTS = 'For Accountants', FOR_SUPPORT_TEAMS = 'For Support Teams', CRM = 'CRM', SCHEDULING = 'Scheduling', - TOOLS = 'Tools' + TOOLS = 'Tools', + PROJECT_MANAGEMENT = 'Project Management', + COMMUNICATION = 'Communication' } export enum IntegrationFilterEnum { @@ -188,65 +199,6 @@ export enum IntegrationFilterEnum { PAID = 'Paid' } -export const DEFAULT_INTEGRATION_PAID_FILTERS = [ - { - label: IntegrationFilterEnum.ALL, - value: 'all' - }, - { - label: IntegrationFilterEnum.FREE, - value: 'false' - }, - { - label: IntegrationFilterEnum.PAID, - value: 'true' - } -]; - -export const DEFAULT_INTEGRATIONS = [ - { - name: IntegrationEnum.HUBSTAFF, - imgSrc: 'hubstaff.svg', - isComingSoon: false, - integrationTypesMap: [ - IntegrationTypeNameEnum.ALL_INTEGRATIONS - ], - order: 1, - navigationUrl: 'hubstaff' - }, - { - name: IntegrationEnum.UPWORK, - imgSrc: 'upwork.svg', - isComingSoon: false, - integrationTypesMap: [ - IntegrationTypeNameEnum.ALL_INTEGRATIONS - ], - order: 2, - navigationUrl: 'upwork' - }, - { - name: IntegrationEnum.GAUZY_AI, - imgSrc: 'gauzy-ai.svg', - isComingSoon: false, - integrationTypesMap: [ - IntegrationTypeNameEnum.ALL_INTEGRATIONS - ], - order: 3, - navigationUrl: 'gauzy-ai' - }, - { - name: 'Import/Export', - imgSrc: 'import-export.svg', - isComingSoon: true, - integrationTypesMap: [ - IntegrationTypeNameEnum.ALL_INTEGRATIONS, - IntegrationTypeNameEnum.CRM - ], - order: 4, - navigationUrl: 'import-export' - } -]; - /** * Hubstaff Integration */ diff --git a/packages/contracts/src/organization-team.model.ts b/packages/contracts/src/organization-team.model.ts index 28038a5c4b9..079cf8c710b 100644 --- a/packages/contracts/src/organization-team.model.ts +++ b/packages/contracts/src/organization-team.model.ts @@ -10,7 +10,7 @@ import { IOrganizationProject } from './organization-projects.model'; export interface IOrganizationTeam extends IBasePerTenantAndOrganizationEntityModel, - IRelationalImageAsset { + IRelationalImageAsset { name: string; color?: string; emoji?: string; @@ -21,13 +21,14 @@ export interface IOrganizationTeam profile_link?: string; members?: IOrganizationTeamEmployee[]; managers?: IOrganizationTeamEmployee[]; + projects?: IOrganizationProject[]; tags?: ITag[]; tasks?: ITask[]; } export interface IOrganizationTeamFindInput extends IBasePerTenantAndOrganizationEntityModel, - IRelationalEmployee { + IRelationalEmployee { name?: string; prefix?: string; public?: boolean; @@ -36,7 +37,7 @@ export interface IOrganizationTeamFindInput export interface IOrganizationTeamCreateInput extends IBasePerTenantAndOrganizationEntityModel, - IRelationalImageAsset { + IRelationalImageAsset { name: string; emoji?: string; teamSize?: string; diff --git a/packages/contracts/src/task.model.ts b/packages/contracts/src/task.model.ts index 4c47bffb193..222937cdd0c 100644 --- a/packages/contracts/src/task.model.ts +++ b/packages/contracts/src/task.model.ts @@ -9,9 +9,9 @@ import { IOrganizationSprint } from './organization-sprint.model'; import { IOrganizationTeam } from './organization-team.model'; import { ITag } from './tag.model'; import { IUser } from './user.model'; -import { TaskStatusEnum } from './task-status.model'; -import { TaskPriorityEnum } from './task-priority.model'; -import { TaskSizeEnum } from './task-size.model'; +import { ITaskStatus, TaskStatusEnum } from './task-status.model'; +import { ITaskPriority, TaskPriorityEnum } from './task-priority.model'; +import { ITaskSize, TaskSizeEnum } from './task-size.model'; export interface ITask extends IBasePerTenantAndOrganizationEntityModel { title: string; @@ -37,6 +37,13 @@ export interface ITask extends IBasePerTenantAndOrganizationEntityModel { parent?: ITask; parentId?: ITask['id']; // Optional field for specifying the parent task ID children?: ITask[]; + + taskStatus?: ITaskStatus; + taskSize?: ITaskSize; + taskPriority?: ITaskPriority; + taskStatusId?: ITaskStatus['id']; + taskSizeId?: ITaskSize['id']; + taskPriorityId?: ITaskPriority['id']; } export interface IGetTaskOptions @@ -48,14 +55,14 @@ export interface IGetTaskByEmployeeOptions extends IBaseRelationsEntityModel { where?: IGetTaskOptions; } -export interface IGetSprintsOptions extends IGetTaskOptions {} +export type IGetSprintsOptions = IGetTaskOptions; export enum TaskParticipantEnum { EMPLOYEES = 'employees', TEAMS = 'teams', } -export interface ITaskCreateInput extends ITask {} +export type ITaskCreateInput = ITask; export interface ITaskUpdateInput extends ITaskCreateInput { id?: string; diff --git a/packages/core/package.json b/packages/core/package.json index 1a821dd0a13..e30e7c85a9f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -48,6 +48,8 @@ "@gauzy/integration-ai": "^0.1.0", "@gauzy/integration-hubstaff": "^0.1.0", "@gauzy/integration-upwork": "^0.1.0", + "@gauzy/integration-github": "^0.1.0", + "@gauzy/integration-jira": "^0.1.0", "@gauzy/plugin": "^0.1.0", "@godaddy/terminus": "^4.5.0", "@grpc/grpc-js": "^1.6.7", diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index ce1d7373b7b..41fefc21c79 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -23,6 +23,7 @@ import { LanguagesEnum } from '@gauzy/contracts'; import { ConfigService, environment } from '@gauzy/config'; import * as path from 'path'; import * as moment from 'moment'; +import { ProbotModule } from '@gauzy/integration-github'; import { CandidateInterviewersModule } from './candidate-interviewers/candidate-interviewers.module'; import { CandidateSkillModule } from './candidate-skill/candidate-skill.module'; import { InvoiceModule } from './invoice/invoice.module'; @@ -84,7 +85,6 @@ import { OrganizationEmploymentTypeModule } from './organization-employment-type import { TimeTrackingModule } from './time-tracking/time-tracking.module'; import { ExpenseCategoriesModule } from './expense-categories/expense-categories.module'; import { UpworkModule } from './upwork/upwork.module'; -import { IntegrationAIModule } from './integration/gauzy-ai/integration-ai.module'; import { CandidateModule } from './candidate/candidate.module'; import { ProductCategoryModule } from './product-category/product-category.module'; import { ProductTypeModule } from './product-type/product-type.module'; @@ -153,7 +153,7 @@ import { EmailResetModule } from './email-reset/email-reset.module'; import { TaskLinkedIssueModule } from './tasks/linked-issue/task-linked-issue.module'; import { OrganizationTaskSettingModule } from './organization-task-setting/organization-task-setting.module'; import { TaskEstimationModule } from './tasks/estimation/task-estimation.module'; -const { unleashConfig } = environment; +const { unleashConfig, github } = environment; if (unleashConfig.url) { const unleashInstanceConfig: UnleashConfig = { @@ -250,6 +250,21 @@ if (environment.sentry && environment.sentry.dsn) { }), ] : []), + + // Probot Configuration + ProbotModule.forRoot({ + isGlobal: true, + path: 'integration/github/webhook', // Webhook URL in GitHub will be: https://api.gauzy.co/api/integration/github/webhook + config: { + /** Client Configuration */ + clientId: github.clientId, + clientSecret: github.clientSecret, + appId: github.appId, + privateKey: github.appPrivateKey, + webhookSecret: github.webhookSecret + }, + }), + ThrottlerModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService): ThrottlerModuleOptions => @@ -353,7 +368,6 @@ if (environment.sentry && environment.sentry.dsn) { FeatureModule, ReportModule, UpworkModule, - IntegrationAIModule, ExpenseCategoriesModule, ProductCategoryModule, ProductTypeModule, diff --git a/packages/core/src/bootstrap/index.ts b/packages/core/src/bootstrap/index.ts index 7c478fcc2e2..a42bab2442d 100644 --- a/packages/core/src/bootstrap/index.ts +++ b/packages/core/src/bootstrap/index.ts @@ -27,9 +27,12 @@ export async function bootstrap( const config = await registerPluginConfig(pluginConfig); const { BootstrapModule } = await import('./bootstrap.module'); - const app = await NestFactory.create(BootstrapModule, { - logger: ['log', 'error', 'warn', 'debug', 'verbose'] - }); + const app = await NestFactory.create( + BootstrapModule, + { + logger: ['log', 'error', 'warn', 'debug', 'verbose'], + } + ); // This will lock all routes and make them accessible by authenticated users only. const reflector = app.get(Reflector); @@ -63,7 +66,7 @@ export async function bootstrap( expressSession({ secret: env.EXPRESS_SESSION_SECRET, resave: true, - saveUninitialized: true + saveUninitialized: true, }) ); @@ -94,7 +97,9 @@ export async function bootstrap( console.log(chalk.green(`Configured Host: ${host}`)); console.log(chalk.green(`Configured Port: ${port}`)); - console.log(chalk.green(`Swagger UI available at http://${host}:${port}/swg`)); + console.log( + chalk.green(`Swagger UI available at http://${host}:${port}/swg`) + ); /** * Dependency injection with class-validator @@ -102,7 +107,9 @@ export async function bootstrap( useContainer(app.select(SharedModule), { fallbackOnErrors: true }); await app.listen(port, host, () => { - console.log(chalk.magenta(`Listening at http://${host}:${port}/${globalPrefix}`)); + console.log( + chalk.magenta(`Listening at http://${host}:${port}/${globalPrefix}`) + ); // Execute Seed For Demo Server if (env.demo) { service.executeDemoSeed(); @@ -127,8 +134,8 @@ export async function registerPluginConfig( */ setConfig({ dbConnectionOptions: { - ...getMigrationsSetting() - } + ...getMigrationsSetting(), + }, }); console.log( @@ -144,8 +151,10 @@ export async function registerPluginConfig( setConfig({ dbConnectionOptions: { entities, - subscribers: coreSubscribers as Array>, - } + subscribers: coreSubscribers as Array< + Type + >, + }, }); let registeredConfig = getConfig(); @@ -164,7 +173,7 @@ export async function registerAllEntities( for (const pluginEntity of pluginEntities) { if (allEntities.find((e) => e.name === pluginEntity.name)) { throw new ConflictException({ - message: `error.${pluginEntity.name} conflict by default entities` + message: `error.${pluginEntity.name} conflict by default entities`, }); } else { allEntities.push(pluginEntity); @@ -179,17 +188,16 @@ export async function registerAllEntities( * @returns */ export function getMigrationsSetting() { - console.log(`Reporting __dirname: ${__dirname}`); //TODO: We need to define some dynamic path here return { migrations: [ // join(__dirname, '../../src/database/migrations/*{.ts,.js}'), - join(__dirname, '../database/migrations/*{.ts,.js}') + join(__dirname, '../database/migrations/*{.ts,.js}'), ], cli: { - migrationsDir: join(__dirname, '../../src/database/migrations') + migrationsDir: join(__dirname, '../../src/database/migrations'), }, - } + }; } diff --git a/packages/core/src/database/migrations/1691494801748-SeedIntegrationTable.ts b/packages/core/src/database/migrations/1691494801748-SeedIntegrationTable.ts index 04066314932..70b251f0484 100644 --- a/packages/core/src/database/migrations/1691494801748-SeedIntegrationTable.ts +++ b/packages/core/src/database/migrations/1691494801748-SeedIntegrationTable.ts @@ -1,13 +1,13 @@ import { MigrationInterface, QueryRunner } from "typeorm"; import { v4 as uuidv4 } from 'uuid'; -import { DEFAULT_INTEGRATIONS, IIntegration, IIntegrationType } from "@gauzy/contracts"; import { getConfig } from "@gauzy/config"; import { copyAssets } from "./../../core/seeds/utils"; +import { DEFAULT_SYSTEM_INTEGRATIONS } from "./../../integration/default-integration"; +import { IntegrationsUtils } from "./../../integration/utils"; export class SeedIntegrationTable1691494801748 implements MigrationInterface { - config = getConfig(); name = 'SeedIntegrationTable1691494801748'; /** @@ -33,12 +33,12 @@ export class SeedIntegrationTable1691494801748 implements MigrationInterface { public async upsertIntegrationsAndIntegrationTypes(queryRunner: QueryRunner): Promise { const destDir = 'integrations'; - for await (const { name, imgSrc, isComingSoon, order, navigationUrl, integrationTypesMap } of DEFAULT_INTEGRATIONS) { + for await (const { name, imgSrc, isComingSoon, order, integrationTypesMap } of DEFAULT_SYSTEM_INTEGRATIONS) { try { const filepath = `integrations/${imgSrc}`; let upsertQuery = ``; - const payload = [name, filepath, isComingSoon, order, navigationUrl]; + const payload = [name, filepath, isComingSoon, order]; if (queryRunner.connection.options.type === 'sqlite') { // For SQLite, manually generate a UUID using uuidv4() @@ -46,33 +46,31 @@ export class SeedIntegrationTable1691494801748 implements MigrationInterface { upsertQuery = ` INSERT INTO integration ( - "name", "imgSrc", "isComingSoon", "order", "navigationUrl", "id" + "name", "imgSrc", "isComingSoon", "order", "id" ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, $4, $5 ) ON CONFLICT(name) DO UPDATE SET "imgSrc" = $2, "isComingSoon" = $3, - "order" = $4, - "navigationUrl" = $5 + "order" = $4 RETURNING id; `; } else { upsertQuery = ` INSERT INTO "integration" ( - "name", "imgSrc", "isComingSoon", "order", "navigationUrl" + "name", "imgSrc", "isComingSoon", "order" ) VALUES ( - $1, $2, $3, $4, $5 + $1, $2, $3, $4 ) ON CONFLICT(name) DO UPDATE SET "imgSrc" = $2, "isComingSoon" = $3, - "order" = $4, - "navigationUrl" = $5 + "order" = $4 RETURNING id; `; } @@ -80,70 +78,17 @@ export class SeedIntegrationTable1691494801748 implements MigrationInterface { const [integration] = await queryRunner.query(upsertQuery, payload); // Step 3: Insert entry in join table to associate Integration with IntegrationType - await this.syncIntegrationType( + await IntegrationsUtils.syncIntegrationType( queryRunner, integration, - await this.getIntegrationTypeByName(queryRunner, integrationTypesMap) - ) + await IntegrationsUtils.getIntegrationTypeByName(queryRunner, integrationTypesMap) + ); - copyAssets(imgSrc, this.config, destDir); + copyAssets(imgSrc, getConfig(), destDir); } catch (error) { // since we have errors let's rollback changes we made console.log(`Error while updating integration: (${name}) in production server`, error); } } } - - /** - * - * @param queryRunner - * @param integrationTypesMap - * @returns - */ - private async getIntegrationTypeByName( - queryRunner: QueryRunner, - integrationTypeNames: any[] - ): Promise { - try { - return await queryRunner.query(`SELECT * FROM "integration_type" WHERE "integration_type"."name" IN ('${integrationTypeNames.join("','")}')`); - } catch (error) { - console.log('Error while querying integration types:', error); - return []; - } - } - - /** - * - * - * @param queryRunner - * @param integration - * @param integrationTypes - */ - private async syncIntegrationType( - queryRunner: QueryRunner, - integration: IIntegration, - integrationTypes: IIntegrationType[] - ) { - if (integration) { - const integrationId = integration.id; - for await (const integrationType of integrationTypes) { - const insertPivotQuery = ` - INSERT INTO "integration_integration_type" ( - "integrationId", - "integrationTypeId" - ) - SELECT - $1, $2 - WHERE NOT EXISTS ( - SELECT 1 - FROM "integration_integration_type" - WHERE - "integrationId" = $1 AND - "integrationTypeId" = $2 - ) - `; - await queryRunner.query(insertPivotQuery, [integrationId, integrationType.id]); - } - } - } } diff --git a/packages/core/src/database/migrations/1692171665427-SeedIntegrationTable.ts b/packages/core/src/database/migrations/1692171665427-SeedIntegrationTable.ts index 7b6d3437a98..32ab6d83c55 100644 --- a/packages/core/src/database/migrations/1692171665427-SeedIntegrationTable.ts +++ b/packages/core/src/database/migrations/1692171665427-SeedIntegrationTable.ts @@ -1,12 +1,13 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -import { DEFAULT_INTEGRATIONS } from "@gauzy/contracts"; +import { v4 as uuidv4 } from 'uuid'; import { getConfig } from "@gauzy/config"; import { copyAssets } from "./../../core/seeds/utils"; +import { DEFAULT_AI_INTEGRATIONS } from "./../../integration/default-integration"; +import { IntegrationsUtils } from "./../../integration/utils"; export class SeedIntegrationTable1692171665427 implements MigrationInterface { - config = getConfig(); name = 'SeedIntegrationTable1692171665427'; /** @@ -15,7 +16,7 @@ export class SeedIntegrationTable1692171665427 implements MigrationInterface { * @param queryRunner */ public async up(queryRunner: QueryRunner): Promise { - await this.upsertIntegrations(queryRunner); + await this.upsertIntegrationsAndIntegrationTypes(queryRunner); } /** @@ -29,27 +30,61 @@ export class SeedIntegrationTable1692171665427 implements MigrationInterface { * * @param queryRunner */ - public async upsertIntegrations(queryRunner: QueryRunner): Promise { + public async upsertIntegrationsAndIntegrationTypes(queryRunner: QueryRunner): Promise { const destDir = 'integrations'; - for await (const { name, imgSrc, navigationUrl } of DEFAULT_INTEGRATIONS) { + + for await (const { name, imgSrc, isComingSoon, order, integrationTypesMap } of DEFAULT_AI_INTEGRATIONS) { try { const filepath = `integrations/${imgSrc}`; - const payload = [name, filepath, navigationUrl]; - - const upsertQuery = ` - INSERT INTO integration ( - "name", "imgSrc", "navigationUrl" - ) - VALUES ( - $1, $2, $3 - ) - ON CONFLICT(name) DO UPDATE - SET - "imgSrc" = $2, - "navigationUrl" = $3; - `; - await queryRunner.query(upsertQuery, payload); - copyAssets(imgSrc, this.config, destDir); + + let upsertQuery = ``; + const payload = [name, filepath, isComingSoon, order]; + + if (queryRunner.connection.options.type === 'sqlite') { + // For SQLite, manually generate a UUID using uuidv4() + const generatedId = uuidv4(); payload.push(generatedId); + + upsertQuery = ` + INSERT INTO integration ( + "name", "imgSrc", "isComingSoon", "order", "id" + ) + VALUES ( + $1, $2, $3, $4, $5 + ) + ON CONFLICT(name) DO UPDATE + SET + "imgSrc" = $2, + "isComingSoon" = $3, + "order" = $4 + RETURNING id; + `; + } else { + upsertQuery = ` + INSERT INTO "integration" ( + "name", "imgSrc", "isComingSoon", "order" + ) + VALUES ( + $1, $2, $3, $4 + ) + ON CONFLICT(name) DO UPDATE + SET + "imgSrc" = $2, + "isComingSoon" = $3, + "order" = $4 + RETURNING id; + `; + } + + const [integration] = await queryRunner.query(upsertQuery, payload); + + // Step 3: Insert entry in join table to associate Integration with IntegrationType + await IntegrationsUtils.syncIntegrationType( + queryRunner, + integration, + await IntegrationsUtils.getIntegrationTypeByName(queryRunner, integrationTypesMap) + ); + + copyAssets(imgSrc, getConfig(), destDir); } catch (error) { // since we have errors let's rollback changes we made console.log(`Error while updating integration: (${name}) in production server`, error); diff --git a/packages/core/src/database/migrations/1695023907817-AddColumnsToTheIntegrationTable.ts b/packages/core/src/database/migrations/1695023907817-AddColumnsToTheIntegrationTable.ts new file mode 100644 index 00000000000..bac23b5d526 --- /dev/null +++ b/packages/core/src/database/migrations/1695023907817-AddColumnsToTheIntegrationTable.ts @@ -0,0 +1,85 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import * as chalk from 'chalk'; +export class AddColumnsToTheIntegrationTable1695023907817 implements MigrationInterface { + + name = 'AddColumnsToTheIntegrationTable1695023907817'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(chalk.magenta(`AddColumnsToTheIntegrationTable1695023907817 start running!`)); + if (queryRunner.connection.options.type === 'sqlite') { + await this.sqliteUpQueryRunner(queryRunner); + } else { + await this.postgresUpQueryRunner(queryRunner); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + if (queryRunner.connection.options.type === 'sqlite') { + await this.sqliteDownQueryRunner(queryRunner); + } else { + await this.postgresDownQueryRunner(queryRunner); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "integration" RENAME COLUMN "navigationUrl" TO "redirectUrl"`); + await queryRunner.query(`ALTER TABLE "integration" ADD "provider" character varying`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "integration" DROP COLUMN "provider"`); + await queryRunner.query(`ALTER TABLE "integration" RENAME COLUMN "redirectUrl" TO "navigationUrl"`); + } + + /** + * SqliteDB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_integration" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "imgSrc" varchar, "isComingSoon" boolean NOT NULL DEFAULT (0), "isPaid" boolean NOT NULL DEFAULT (0), "version" varchar, "docUrl" varchar, "isFreeTrial" boolean NOT NULL DEFAULT (0), "freeTrialPeriod" numeric DEFAULT (0), "order" integer, CONSTRAINT "UQ_938c19d92ad3f290ff5fc163531" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "temporary_integration"("id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order") SELECT "id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order" FROM "integration"`); + await queryRunner.query(`DROP TABLE "integration"`); + await queryRunner.query(`ALTER TABLE "temporary_integration" RENAME TO "integration"`); + await queryRunner.query(`CREATE TABLE "temporary_integration" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "imgSrc" varchar, "isComingSoon" boolean NOT NULL DEFAULT (0), "isPaid" boolean NOT NULL DEFAULT (0), "version" varchar, "docUrl" varchar, "isFreeTrial" boolean NOT NULL DEFAULT (0), "freeTrialPeriod" numeric DEFAULT (0), "order" integer, "provider" varchar, "redirectUrl" varchar, CONSTRAINT "UQ_938c19d92ad3f290ff5fc163531" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "temporary_integration"("id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order") SELECT "id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order" FROM "integration"`); + await queryRunner.query(`DROP TABLE "integration"`); + await queryRunner.query(`ALTER TABLE "temporary_integration" RENAME TO "integration"`); + } + + /** + * SqliteDB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "integration" RENAME TO "temporary_integration"`); + await queryRunner.query(`CREATE TABLE "integration" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "imgSrc" varchar, "isComingSoon" boolean NOT NULL DEFAULT (0), "isPaid" boolean NOT NULL DEFAULT (0), "version" varchar, "docUrl" varchar, "isFreeTrial" boolean NOT NULL DEFAULT (0), "freeTrialPeriod" numeric DEFAULT (0), "order" integer, CONSTRAINT "UQ_938c19d92ad3f290ff5fc163531" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "integration"("id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order") SELECT "id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order" FROM "temporary_integration"`); + await queryRunner.query(`DROP TABLE "temporary_integration"`); + await queryRunner.query(`ALTER TABLE "integration" RENAME TO "temporary_integration"`); + await queryRunner.query(`CREATE TABLE "integration" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "imgSrc" varchar, "isComingSoon" boolean NOT NULL DEFAULT (0), "isPaid" boolean NOT NULL DEFAULT (0), "version" varchar, "docUrl" varchar, "isFreeTrial" boolean NOT NULL DEFAULT (0), "freeTrialPeriod" numeric DEFAULT (0), "order" integer, "navigationUrl" varchar, CONSTRAINT "UQ_938c19d92ad3f290ff5fc163531" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "integration"("id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order") SELECT "id", "createdAt", "updatedAt", "name", "imgSrc", "isComingSoon", "isPaid", "version", "docUrl", "isFreeTrial", "freeTrialPeriod", "order" FROM "temporary_integration"`); + await queryRunner.query(`DROP TABLE "temporary_integration"`); + } +} diff --git a/packages/core/src/database/migrations/1695111838783-AddColumnsToTheIntegrationTypeTable.ts b/packages/core/src/database/migrations/1695111838783-AddColumnsToTheIntegrationTypeTable.ts new file mode 100644 index 00000000000..93539e3b896 --- /dev/null +++ b/packages/core/src/database/migrations/1695111838783-AddColumnsToTheIntegrationTypeTable.ts @@ -0,0 +1,87 @@ + +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddColumnsToTheIntegrationTypeTable1695111838783 implements MigrationInterface { + + name = 'AddColumnsToTheIntegrationTypeTable1695111838783'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + if (queryRunner.connection.options.type === 'sqlite') { + await this.sqliteUpQueryRunner(queryRunner); + } else { + await this.postgresUpQueryRunner(queryRunner); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + if (queryRunner.connection.options.type === 'sqlite') { + await this.sqliteDownQueryRunner(queryRunner); + } else { + await this.postgresDownQueryRunner(queryRunner); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "integration_type" ADD "description" character varying`); + await queryRunner.query(`ALTER TABLE "integration_type" ADD "icon" character varying`); + await queryRunner.query(`ALTER TABLE "integration_type" ADD CONSTRAINT "UQ_83443d669822bbbf2bd0ebdacd7" UNIQUE ("name")`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "integration_type" DROP CONSTRAINT "UQ_83443d669822bbbf2bd0ebdacd7"`); + await queryRunner.query(`ALTER TABLE "integration_type" DROP COLUMN "icon"`); + await queryRunner.query(`ALTER TABLE "integration_type" DROP COLUMN "description"`); + } + + /** + * SqliteDB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_integration_type" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "groupName" varchar NOT NULL, "order" integer NOT NULL, "description" varchar, "icon" varchar)`); + await queryRunner.query(`INSERT INTO "temporary_integration_type"("id", "createdAt", "updatedAt", "name", "groupName", "order") SELECT "id", "createdAt", "updatedAt", "name", "groupName", "order" FROM "integration_type"`); + await queryRunner.query(`DROP TABLE "integration_type"`); + await queryRunner.query(`ALTER TABLE "temporary_integration_type" RENAME TO "integration_type"`); + await queryRunner.query(`CREATE TABLE "temporary_integration_type" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "groupName" varchar NOT NULL, "order" integer NOT NULL, "description" varchar, "icon" varchar, CONSTRAINT "UQ_be0d67f4df84f036fab2ed89c47" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "temporary_integration_type"("id", "createdAt", "updatedAt", "name", "groupName", "order", "description", "icon") SELECT "id", "createdAt", "updatedAt", "name", "groupName", "order", "description", "icon" FROM "integration_type"`); + await queryRunner.query(`DROP TABLE "integration_type"`); + await queryRunner.query(`ALTER TABLE "temporary_integration_type" RENAME TO "integration_type"`); + } + + /** + * SqliteDB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "integration_type" RENAME TO "temporary_integration_type"`); + await queryRunner.query(`CREATE TABLE "integration_type" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "groupName" varchar NOT NULL, "order" integer NOT NULL, "description" varchar, "icon" varchar)`); + await queryRunner.query(`INSERT INTO "integration_type"("id", "createdAt", "updatedAt", "name", "groupName", "order", "description", "icon") SELECT "id", "createdAt", "updatedAt", "name", "groupName", "order", "description", "icon" FROM "temporary_integration_type"`); + await queryRunner.query(`DROP TABLE "temporary_integration_type"`); + await queryRunner.query(`ALTER TABLE "integration_type" RENAME TO "temporary_integration_type"`); + await queryRunner.query(`CREATE TABLE "integration_type" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, "groupName" varchar NOT NULL, "order" integer NOT NULL)`); + await queryRunner.query(`INSERT INTO "integration_type"("id", "createdAt", "updatedAt", "name", "groupName", "order") SELECT "id", "createdAt", "updatedAt", "name", "groupName", "order" FROM "temporary_integration_type"`); + await queryRunner.query(`DROP TABLE "temporary_integration_type"`); + } +} diff --git a/packages/core/src/database/migrations/1695112275840-SeedIntegrationsAndIntegrationTypes.ts b/packages/core/src/database/migrations/1695112275840-SeedIntegrationsAndIntegrationTypes.ts new file mode 100644 index 00000000000..56791f3e9d3 --- /dev/null +++ b/packages/core/src/database/migrations/1695112275840-SeedIntegrationsAndIntegrationTypes.ts @@ -0,0 +1,27 @@ + +import { MigrationInterface, QueryRunner } from "typeorm"; +import { IntegrationTypeEnum } from "@gauzy/contracts"; +import { DEFAULT_INTEGRATIONS, PROJECT_MANAGE_DEFAULT_INTEGRATIONS } from "./../../integration/default-integration"; +import { IntegrationsUtils } from "./../../integration/utils"; + +export class SeedIntegrationsAndIntegrationTypes1695112275840 implements MigrationInterface { + + name = 'SeedIntegrationsAndIntegrationTypes1695112275840'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + await IntegrationsUtils.upsertIntegrationTypes(queryRunner, [IntegrationTypeEnum.PROJECT_MANAGEMENT]); + await IntegrationsUtils.upsertIntegrationsAndIntegrationTypes(queryRunner, PROJECT_MANAGE_DEFAULT_INTEGRATIONS); + await IntegrationsUtils.upsertIntegrationsAndIntegrationTypes(queryRunner, DEFAULT_INTEGRATIONS); + } + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { } +} diff --git a/packages/core/src/database/migrations/1695570009125-AlterTableTask.ts b/packages/core/src/database/migrations/1695570009125-AlterTableTask.ts new file mode 100644 index 00000000000..64d176c8948 --- /dev/null +++ b/packages/core/src/database/migrations/1695570009125-AlterTableTask.ts @@ -0,0 +1,347 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AlterTableTask1695570009125 implements MigrationInterface { + name = 'AlterTableTask1695570009125'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + if (queryRunner.connection.options.type === 'sqlite') { + await this.sqliteUpQueryRunner(queryRunner); + } else { + await this.postgresUpQueryRunner(queryRunner); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + if (queryRunner.connection.options.type === 'sqlite') { + await this.sqliteDownQueryRunner(queryRunner); + } else { + await this.postgresDownQueryRunner(queryRunner); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "task" ADD "taskStatusId" uuid`); + await queryRunner.query(`ALTER TABLE "task" ADD "taskSizeId" uuid`); + await queryRunner.query(`ALTER TABLE "task" ADD "taskPriorityId" uuid`); + await queryRunner.query( + `CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") ` + ); + await queryRunner.query( + `ALTER TABLE "task" ADD CONSTRAINT "FK_0cbe714983eb0aae5feeee8212b" FOREIGN KEY ("taskStatusId") REFERENCES "task_status"("id") ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "task" ADD CONSTRAINT "FK_2f4bdd2593fd6038aaa91fd1076" FOREIGN KEY ("taskSizeId") REFERENCES "task_size"("id") ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "task" ADD CONSTRAINT "FK_b8616deefe44d0622233e73fbf9" FOREIGN KEY ("taskPriorityId") REFERENCES "task_priority"("id") ON DELETE SET NULL ON UPDATE NO ACTION` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner( + queryRunner: QueryRunner + ): Promise { + await queryRunner.query( + `ALTER TABLE "task" DROP CONSTRAINT "FK_b8616deefe44d0622233e73fbf9"` + ); + await queryRunner.query( + `ALTER TABLE "task" DROP CONSTRAINT "FK_2f4bdd2593fd6038aaa91fd1076"` + ); + await queryRunner.query( + `ALTER TABLE "task" DROP CONSTRAINT "FK_0cbe714983eb0aae5feeee8212b"` + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_b8616deefe44d0622233e73fbf"` + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_2f4bdd2593fd6038aaa91fd107"` + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_0cbe714983eb0aae5feeee8212"` + ); + await queryRunner.query( + `ALTER TABLE "task" DROP COLUMN "taskPriorityId"` + ); + await queryRunner.query(`ALTER TABLE "task" DROP COLUMN "taskSizeId"`); + await queryRunner.query( + `ALTER TABLE "task" DROP COLUMN "taskStatusId"` + ); + } + + /** + * SqliteDB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query( + `CREATE TABLE "temporary_task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, "taskStatusId" varchar, "taskSizeId" varchar, "taskPriorityId" varchar, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId" FROM "task"` + ); + await queryRunner.query(`DROP TABLE "task"`); + await queryRunner.query( + `ALTER TABLE "temporary_task" RENAME TO "task"` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") ` + ); + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query(`DROP INDEX "IDX_0cbe714983eb0aae5feeee8212"`); + await queryRunner.query(`DROP INDEX "IDX_2f4bdd2593fd6038aaa91fd107"`); + await queryRunner.query(`DROP INDEX "IDX_b8616deefe44d0622233e73fbf"`); + await queryRunner.query( + `CREATE TABLE "temporary_task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, "taskStatusId" varchar, "taskSizeId" varchar, "taskPriorityId" varchar, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_0cbe714983eb0aae5feeee8212b" FOREIGN KEY ("taskStatusId") REFERENCES "task_status" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_2f4bdd2593fd6038aaa91fd1076" FOREIGN KEY ("taskSizeId") REFERENCES "task_size" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_b8616deefe44d0622233e73fbf9" FOREIGN KEY ("taskPriorityId") REFERENCES "task_priority" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId" FROM "task"` + ); + await queryRunner.query(`DROP TABLE "task"`); + await queryRunner.query( + `ALTER TABLE "temporary_task" RENAME TO "task"` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") ` + ); + } + + /** + * SqliteDB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_b8616deefe44d0622233e73fbf"`); + await queryRunner.query(`DROP INDEX "IDX_2f4bdd2593fd6038aaa91fd107"`); + await queryRunner.query(`DROP INDEX "IDX_0cbe714983eb0aae5feeee8212"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query( + `ALTER TABLE "task" RENAME TO "temporary_task"` + ); + await queryRunner.query( + `CREATE TABLE "task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, "taskStatusId" varchar, "taskSizeId" varchar, "taskPriorityId" varchar, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId", "taskStatusId", "taskSizeId", "taskPriorityId" FROM "temporary_task"` + ); + await queryRunner.query(`DROP TABLE "temporary_task"`); + await queryRunner.query( + `CREATE INDEX "IDX_b8616deefe44d0622233e73fbf" ON "task" ("taskPriorityId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2f4bdd2593fd6038aaa91fd107" ON "task" ("taskSizeId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_0cbe714983eb0aae5feeee8212" ON "task" ("taskStatusId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") ` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") ` + ); + await queryRunner.query(`DROP INDEX "IDX_b8616deefe44d0622233e73fbf"`); + await queryRunner.query(`DROP INDEX "IDX_2f4bdd2593fd6038aaa91fd107"`); + await queryRunner.query(`DROP INDEX "IDX_0cbe714983eb0aae5feeee8212"`); + await queryRunner.query(`DROP INDEX "IDX_ed5441fb13e82854a994da5a78"`); + await queryRunner.query(`DROP INDEX "IDX_7127880d6fae956ecc1c84ac31"`); + await queryRunner.query(`DROP INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1"`); + await queryRunner.query(`DROP INDEX "IDX_2fe7a278e6f08d2be55740a939"`); + await queryRunner.query(`DROP INDEX "IDX_e91cbff3d206f150ccc14d0c3a"`); + await queryRunner.query(`DROP INDEX "IDX_5b0272d923a31c972bed1a1ac4"`); + await queryRunner.query(`DROP INDEX "IDX_3797a20ef5553ae87af126bc2f"`); + await queryRunner.query(`DROP INDEX "IDX_94fe6b3a5aec5f85427df4f8cd"`); + await queryRunner.query(`DROP INDEX "IDX_1e1f64696aa3a26d3e12c840e5"`); + await queryRunner.query(`DROP INDEX "taskNumber"`); + await queryRunner.query( + `ALTER TABLE "task" RENAME TO "temporary_task"` + ); + await queryRunner.query( + `CREATE TABLE "task" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "title" varchar NOT NULL, "description" varchar, "status" varchar, "estimate" integer, "dueDate" datetime, "projectId" varchar, "creatorId" varchar, "organizationSprintId" varchar, "number" integer, "prefix" varchar, "priority" varchar, "size" varchar, "public" boolean DEFAULT (1), "startDate" datetime, "resolvedAt" datetime, "version" varchar, "issueType" varchar, "parentId" varchar, CONSTRAINT "FK_8c9920b5fb32c3d8453f64b705c" FOREIGN KEY ("parentId") REFERENCES "task" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_1e1f64696aa3a26d3e12c840e55" FOREIGN KEY ("organizationSprintId") REFERENCES "organization_sprint" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_94fe6b3a5aec5f85427df4f8cd7" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_3797a20ef5553ae87af126bc2fe" FOREIGN KEY ("projectId") REFERENCES "organization_project" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_5b0272d923a31c972bed1a1ac4d" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_e91cbff3d206f150ccc14d0c3a1" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "task"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "title", "description", "status", "estimate", "dueDate", "projectId", "creatorId", "organizationSprintId", "number", "prefix", "priority", "size", "public", "startDate", "resolvedAt", "version", "issueType", "parentId" FROM "temporary_task"` + ); + await queryRunner.query(`DROP TABLE "temporary_task"`); + await queryRunner.query( + `CREATE INDEX "IDX_ed5441fb13e82854a994da5a78" ON "task" ("issueType") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7127880d6fae956ecc1c84ac31" ON "task" ("size") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_f092f3386f10f2e2ef5b0b6ad1" ON "task" ("priority") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2fe7a278e6f08d2be55740a939" ON "task" ("status") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_e91cbff3d206f150ccc14d0c3a" ON "task" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5b0272d923a31c972bed1a1ac4" ON "task" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_3797a20ef5553ae87af126bc2f" ON "task" ("projectId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_94fe6b3a5aec5f85427df4f8cd" ON "task" ("creatorId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_1e1f64696aa3a26d3e12c840e5" ON "task" ("organizationSprintId") ` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "taskNumber" ON "task" ("projectId", "number") ` + ); + } +} diff --git a/packages/core/src/integration-entity-setting/integration-entity-setting.entity.ts b/packages/core/src/integration-entity-setting/integration-entity-setting.entity.ts index e962cc405d2..6e8f80c1429 100644 --- a/packages/core/src/integration-entity-setting/integration-entity-setting.entity.ts +++ b/packages/core/src/integration-entity-setting/integration-entity-setting.entity.ts @@ -44,8 +44,9 @@ export class IntegrationEntitySetting extends TenantOrganizationBaseEntity imple * IntegrationTenant */ @ApiPropertyOptional({ type: () => IntegrationTenant }) - @ManyToOne(() => IntegrationTenant, (integration) => integration.entitySettings, { - onDelete: 'CASCADE' + @ManyToOne(() => IntegrationTenant, (it) => it.entitySettings, { + /** Database cascade action on delete. */ + onDelete: 'CASCADE', }) @JoinColumn() integration?: IIntegrationTenant; diff --git a/packages/core/src/integration-map/integration-map.entity.ts b/packages/core/src/integration-map/integration-map.entity.ts index 3dd2a3c8ba3..e215ba1d7df 100644 --- a/packages/core/src/integration-map/integration-map.entity.ts +++ b/packages/core/src/integration-map/integration-map.entity.ts @@ -33,7 +33,8 @@ export class IntegrationMap extends TenantOrganizationBaseEntity implements IInt */ @ApiProperty({ type: () => IntegrationTenant }) @ManyToOne(() => IntegrationTenant, (it) => it.entityMaps, { - onDelete: 'CASCADE' + /** Database cascade action on delete. */ + onDelete: 'CASCADE', }) @JoinColumn() integration: IIntegrationTenant; diff --git a/packages/core/src/integration-setting/integration-setting.entity.ts b/packages/core/src/integration-setting/integration-setting.entity.ts index a68eea63c32..9c0632463f0 100644 --- a/packages/core/src/integration-setting/integration-setting.entity.ts +++ b/packages/core/src/integration-setting/integration-setting.entity.ts @@ -28,8 +28,9 @@ export class IntegrationSetting extends TenantOrganizationBaseEntity implements * IntegrationTenant */ @ApiProperty({ type: () => IntegrationTenant }) - @ManyToOne(() => IntegrationTenant, (integrationTenant) => integrationTenant.settings, { - onDelete: 'CASCADE' + @ManyToOne(() => IntegrationTenant, (it) => it.settings, { + /** Database cascade action on delete. */ + onDelete: 'CASCADE', }) @JoinColumn() integration?: IntegrationTenant; diff --git a/packages/core/src/integration-tenant/commands/handlers/index.ts b/packages/core/src/integration-tenant/commands/handlers/index.ts index ceb131ba8e0..c17d21b0cb7 100644 --- a/packages/core/src/integration-tenant/commands/handlers/index.ts +++ b/packages/core/src/integration-tenant/commands/handlers/index.ts @@ -1,9 +1,11 @@ import { IntegrationTenantCreateHandler } from './integration-tenant.create.handler'; +import { IntegrationTenantFirstOrCreateHandler } from './integration-tenant-first-or-create.handler'; import { IntegrationTenantGetHandler } from './integration-tenant.get.handler'; import { IntegrationTenantUpdateHandler } from './integration-tenant.update.handler'; export const CommandHandlers = [ IntegrationTenantCreateHandler, + IntegrationTenantFirstOrCreateHandler, IntegrationTenantGetHandler, - IntegrationTenantUpdateHandler + IntegrationTenantUpdateHandler, ]; diff --git a/packages/core/src/integration-tenant/commands/handlers/integration-tenant-first-or-create.handler.ts b/packages/core/src/integration-tenant/commands/handlers/integration-tenant-first-or-create.handler.ts new file mode 100644 index 00000000000..ddeea89d1dd --- /dev/null +++ b/packages/core/src/integration-tenant/commands/handlers/integration-tenant-first-or-create.handler.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { IntegrationTenantCreateCommand } from '../integration-tenant.create.command'; +import { IntegrationTenantFirstOrCreateCommand } from '../integration-tenant-first-or-create.command'; +import { IntegrationTenantGetCommand } from '../integration-tenant.get.command'; + +@Injectable() +@CommandHandler(IntegrationTenantFirstOrCreateCommand) +export class IntegrationTenantFirstOrCreateHandler implements ICommandHandler { + + constructor( + private readonly _commandBus: CommandBus + ) { } + + public async execute( + command: IntegrationTenantFirstOrCreateCommand + ) { + const { options, input } = command; + try { + return await this._commandBus.execute( + new IntegrationTenantGetCommand({ + where: { + ...options + } + }) + ); + } catch (error) { + return await this._commandBus.execute( + new IntegrationTenantCreateCommand(input) + ); + } + } +} diff --git a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.create.handler.ts b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.create.handler.ts index 68e1d04c9ae..e2760d6f0d0 100644 --- a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.create.handler.ts +++ b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.create.handler.ts @@ -1,4 +1,4 @@ -import { BadRequestException } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { IntegrationTenantCreateCommand } from '../../commands/integration-tenant.create.command'; import { IntegrationTenantService } from '../../integration-tenant.service'; @@ -18,7 +18,9 @@ export class IntegrationTenantCreateHandler implements ICommandHandler { - constructor(private _integrationTenantService: IntegrationTenantService) {} +export class IntegrationTenantGetHandler implements ICommandHandler { + + constructor( + private readonly _integrationTenantService: IntegrationTenantService + ) { } public async execute( command: IntegrationTenantGetCommand ): Promise { - const { input } = command; - return await this._integrationTenantService.findOneByOptions(input); + try { + const { input } = command; + return await this._integrationTenantService.findOneByOptions(input); + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to get integration tenant: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } } } diff --git a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.update.handler.ts b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.update.handler.ts index 4f15ed012b8..22c0197d6e7 100644 --- a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.update.handler.ts +++ b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.update.handler.ts @@ -1,4 +1,4 @@ -import { BadRequestException } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { IntegrationTenantUpdateCommand } from '../../commands'; import { IntegrationTenantService } from '../../integration-tenant.service'; @@ -16,11 +16,13 @@ export class IntegrationTenantUpdateHandler implements ICommandHandler { try { const { id, input } = command; - await this._integrationTenantService.update(id, input); + return await this._integrationTenantService.findOneByIdString(id); } catch (error) { - throw new BadRequestException(error); + // Handle errors and return an appropriate error response + console.log(`Failed to update integration tenant: %s`, error.message); + throw new HttpException(`Failed to update integration tenant: ${error.message}`, HttpStatus.BAD_REQUEST); } } } diff --git a/packages/core/src/integration-tenant/commands/index.ts b/packages/core/src/integration-tenant/commands/index.ts index e3c04db500e..2418cc0e903 100644 --- a/packages/core/src/integration-tenant/commands/index.ts +++ b/packages/core/src/integration-tenant/commands/index.ts @@ -1,3 +1,4 @@ export * from './integration-tenant.create.command'; export * from './integration-tenant.get.command'; export * from './integration-tenant.update.command'; +export * from './integration-tenant-first-or-create.command'; diff --git a/packages/core/src/integration-tenant/commands/integration-tenant-first-or-create.command.ts b/packages/core/src/integration-tenant/commands/integration-tenant-first-or-create.command.ts new file mode 100644 index 00000000000..aa58008bd08 --- /dev/null +++ b/packages/core/src/integration-tenant/commands/integration-tenant-first-or-create.command.ts @@ -0,0 +1,13 @@ +import { ICommand } from '@nestjs/cqrs'; +import { FindOptionsWhere } from 'typeorm'; +import { IIntegrationTenant, } from '@gauzy/contracts'; +import { IntegrationTenant } from '../integration-tenant.entity'; + +export class IntegrationTenantFirstOrCreateCommand implements ICommand { + static readonly type = '[Integration Tenant] First Or Create'; + + constructor( + public readonly options: FindOptionsWhere, + public readonly input: IIntegrationTenant + ) { } +} diff --git a/packages/core/src/integration-tenant/integration-tenant.entity.ts b/packages/core/src/integration-tenant/integration-tenant.entity.ts index 594d10f0aac..86c060c946b 100644 --- a/packages/core/src/integration-tenant/integration-tenant.entity.ts +++ b/packages/core/src/integration-tenant/integration-tenant.entity.ts @@ -29,8 +29,12 @@ export class IntegrationTenant extends TenantOrganizationBaseEntity implements I * Integration */ @ManyToOne(() => Integration, { + + /** Indicates if relation column value can be nullable or not. */ nullable: true, - onDelete: 'CASCADE' + + /** Database cascade action on delete. */ + onDelete: 'CASCADE', }) @JoinColumn() integration?: IIntegration; diff --git a/packages/core/src/integration-tenant/integration-tenant.service.ts b/packages/core/src/integration-tenant/integration-tenant.service.ts index de0df7c6c2e..66585ee4147 100644 --- a/packages/core/src/integration-tenant/integration-tenant.service.ts +++ b/packages/core/src/integration-tenant/integration-tenant.service.ts @@ -9,8 +9,8 @@ import { IIntegrationTenantFindInput, IntegrationEnum } from '@gauzy/contracts'; -import { RequestContext } from './../core/context'; -import { TenantAwareCrudService } from './../core/crud'; +import { RequestContext } from 'core/context'; +import { TenantAwareCrudService } from 'core/crud'; import { IntegrationTenant } from './integration-tenant.entity'; @Injectable() @@ -30,10 +30,9 @@ export class IntegrationTenantService extends TenantAwareCrudService { - const tenantId = RequestContext.currentTenantId(); try { - const { organizationId } = input; - let { entitySettings = [], settings = [] } = input; + const tenantId = RequestContext.currentTenantId() || input.tenantId; + let { organizationId, entitySettings = [], settings = [] } = input; settings = settings.map((item: IIntegrationSetting) => ({ ...item, @@ -50,11 +49,12 @@ export class IntegrationTenantService extends TenantAwareCrudService[ + IntegrationTypeEnum.ALL_INTEGRATIONS + ], + order: 1, + redirectUrl: sluggable(IntegrationEnum.HUBSTAFF), + provider: IntegrationEnum.HUBSTAFF + }, + { + name: IntegrationEnum.UPWORK, + imgSrc: 'upwork.svg', + isComingSoon: false, + integrationTypesMap: [ + IntegrationTypeEnum.ALL_INTEGRATIONS + ], + order: 2, + redirectUrl: sluggable(IntegrationEnum.UPWORK), + provider: IntegrationEnum.UPWORK + }, + { + name: 'Import/Export', + imgSrc: 'import-export.svg', + isComingSoon: true, + integrationTypesMap: [ + IntegrationTypeEnum.ALL_INTEGRATIONS, + IntegrationTypeEnum.CRM + ], + order: 6, + redirectUrl: sluggable(IntegrationEnum.IMPORT_EXPORT), + provider: IntegrationEnum.IMPORT_EXPORT + }, +]; + +/** + * + */ +export const DEFAULT_AI_INTEGRATIONS = [ + { + name: 'Gauzy AI', + imgSrc: 'gauzy-ai.svg', + isComingSoon: false, + integrationTypesMap: [ + IntegrationTypeEnum.ALL_INTEGRATIONS + ], + order: 3, + redirectUrl: sluggable(IntegrationEnum.GAUZY_AI), + provider: IntegrationEnum.GAUZY_AI + }, +]; + +/** + * + */ +export const PROJECT_MANAGE_DEFAULT_INTEGRATIONS = [ + { + name: IntegrationEnum.GITHUB, + imgSrc: 'github.svg', + isComingSoon: false, + integrationTypesMap: [ + IntegrationTypeEnum.ALL_INTEGRATIONS, + IntegrationTypeEnum.PROJECT_MANAGEMENT + ], + order: 4, + redirectUrl: sluggable(IntegrationEnum.GITHUB), + provider: IntegrationEnum.GITHUB + }, + { + name: IntegrationEnum.JIRA, + imgSrc: 'jira.svg', + isComingSoon: true, + integrationTypesMap: [ + IntegrationTypeEnum.ALL_INTEGRATIONS, + IntegrationTypeEnum.PROJECT_MANAGEMENT + ], + order: 5, + redirectUrl: sluggable(IntegrationEnum.JIRA), + provider: IntegrationEnum.JIRA + }, +]; + +export const DEFAULT_INTEGRATIONS = [ + ...DEFAULT_SYSTEM_INTEGRATIONS, + ...DEFAULT_AI_INTEGRATIONS, + ...PROJECT_MANAGE_DEFAULT_INTEGRATIONS +]; diff --git a/packages/core/src/integration/gauzy-ai/integration-ai.module.ts b/packages/core/src/integration/gauzy-ai/integration-ai.module.ts index 54668b532bf..9281d1f3e61 100644 --- a/packages/core/src/integration/gauzy-ai/integration-ai.module.ts +++ b/packages/core/src/integration/gauzy-ai/integration-ai.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule, RequestMethod, forwardRef } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { RouterModule } from 'nest-router'; import { GauzyAIModule } from '@gauzy/integration-ai'; @@ -13,12 +13,9 @@ import { EmployeeJobPostController } from './../../employee-job/employee-job.con @Module({ imports: [ - RouterModule.forRoutes([ - { path: '/integrations/gauzy-ai', module: IntegrationAIModule } - ]), TenantModule, UserModule, - IntegrationModule, + forwardRef(() => IntegrationModule), IntegrationTenantModule, CqrsModule, GauzyAIModule.forRoot() diff --git a/packages/core/src/integration/gauzy-ai/integration-ai.service.ts b/packages/core/src/integration/gauzy-ai/integration-ai.service.ts index ce582a356f8..5e2be9f4a19 100644 --- a/packages/core/src/integration/gauzy-ai/integration-ai.service.ts +++ b/packages/core/src/integration/gauzy-ai/integration-ai.service.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { IIntegrationKeySecretPairInput, IIntegrationTenant, IntegrationEnum } from '@gauzy/contracts'; import { RequestContext } from '../../core/context'; -import { IntegrationTenantCreateCommand } from '../../integration-tenant/commands'; +import { IntegrationTenantFirstOrCreateCommand } from '../../integration-tenant/commands'; import { IntegrationService } from './../../integration/integration.service'; @Injectable() export class IntegrationAIService { + private readonly logger = new Logger('IntegrationAIService'); constructor( private readonly _commandBus: CommandBus, @@ -23,34 +24,53 @@ export class IntegrationAIService { input: IIntegrationKeySecretPairInput ): Promise { - const tenantId = RequestContext.currentTenantId(); - const { client_id, client_secret, organizationId } = input; + try { + const tenantId = RequestContext.currentTenantId(); + const { client_id, client_secret, organizationId } = input; - const integration = await this._integrationService.findOneByOptions({ - where: { - name: IntegrationEnum.GAUZY_AI - } - }); + const integration = await this._integrationService.findOneByOptions({ + where: { + provider: IntegrationEnum.GAUZY_AI + } + }); - return await this._commandBus.execute( - new IntegrationTenantCreateCommand({ - name: IntegrationEnum.GAUZY_AI, - integration, - entitySettings: [], - settings: [ + /** Execute the command to create the integration tenant settings */ + return await this._commandBus.execute( + new IntegrationTenantFirstOrCreateCommand( { - settingsName: 'apiKey', - settingsValue: client_id, - + name: IntegrationEnum.GAUZY_AI, + integration: { + provider: IntegrationEnum.GAUZY_AI + }, + tenantId, + organizationId, }, { - settingsName: 'apiSecret', - settingsValue: client_secret + name: IntegrationEnum.GAUZY_AI, + integration, + organizationId, + tenantId, + entitySettings: [], + settings: [ + { + settingsName: 'apiKey', + settingsValue: client_id + }, + { + settingsName: 'apiSecret', + settingsValue: client_secret + } + ].map((setting) => ({ + ...setting, + tenantId, + organizationId, + })) } - ], - organizationId, - tenantId, - }) - ); + ) + ); + } catch (error) { + this.logger.error(`Error while creating ${IntegrationEnum.GAUZY_AI} integration settings`, error?.message); + throw new Error(`Failed to add ${IntegrationEnum.GAUZY_AI} integration`); + } } } diff --git a/packages/core/src/integration/github/dto/github-app-install.dto.ts b/packages/core/src/integration/github/dto/github-app-install.dto.ts new file mode 100644 index 00000000000..accc990dede --- /dev/null +++ b/packages/core/src/integration/github/dto/github-app-install.dto.ts @@ -0,0 +1,17 @@ +import { IGithubAppInstallInput } from "@gauzy/contracts"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; +import { TenantOrganizationBaseDTO } from "core/dto"; + +export class GithubAppInstallDTO extends TenantOrganizationBaseDTO implements IGithubAppInstallInput { + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsString() + readonly installation_id: string; + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsString() + readonly setup_action: string; +} diff --git a/packages/core/src/integration/github/dto/index.ts b/packages/core/src/integration/github/dto/index.ts new file mode 100644 index 00000000000..95f85018e37 --- /dev/null +++ b/packages/core/src/integration/github/dto/index.ts @@ -0,0 +1 @@ +export * from './github-app-install.dto'; diff --git a/packages/core/src/integration/github/github-authorization.controller.ts b/packages/core/src/integration/github/github-authorization.controller.ts new file mode 100644 index 00000000000..929ba09927f --- /dev/null +++ b/packages/core/src/integration/github/github-authorization.controller.ts @@ -0,0 +1,46 @@ +import { Controller, Get, HttpException, HttpStatus, Query, Res } from '@nestjs/common'; +import { Response } from 'express'; +import { ConfigService } from '@gauzy/config'; +import { IGithubAppInstallInput } from '@gauzy/contracts'; +import { IGithubConfig, Public } from '@gauzy/common'; + +@Controller() +export class GitHubAuthorizationController { + constructor( + private readonly _config: ConfigService + ) { } + + /** + * + * @param query + * @param response + */ + @Public() + @Get('/callback') + async githubIntegrationPostInstallCallback( + @Query() query: IGithubAppInstallInput, + @Res() response: Response + ) { + try { + // Validate the input data (You can use class-validator for validation) + if (!query || !query.installation_id || !query.setup_action || !query.state) { + throw new HttpException('Invalid installation query data', HttpStatus.BAD_REQUEST); + } + + /** Github Config Options */ + const github = this._config.get('github') as IGithubConfig; + + /** Construct the redirect URL with query parameters */ + const urlParams = new URLSearchParams(); + urlParams.append('installation_id', query.installation_id); + urlParams.append('setup_action', query.setup_action); + urlParams.append('state', query.state); + + /** Redirect to the URL */ + return response.redirect(`${github.postInstallUrl}?${urlParams.toString()}`); + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to add GitHub installation: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/packages/core/src/integration/github/github-integration.controller.ts b/packages/core/src/integration/github/github-integration.controller.ts new file mode 100644 index 00000000000..0dcad371be0 --- /dev/null +++ b/packages/core/src/integration/github/github-integration.controller.ts @@ -0,0 +1,151 @@ +import { Controller, Get, HttpException, HttpStatus, Logger, Param, Query, Req, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common'; +import { Request } from 'express'; +import { OctokitResponse, OctokitService } from '@gauzy/integration-github'; +import { IGithubIssue, IGithubRepositoryResponse, PermissionsEnum } from '@gauzy/contracts'; +import { PermissionGuard, TenantPermissionGuard } from 'shared/guards'; +import { Permissions } from 'shared/decorators'; +import { TenantOrganizationBaseDTO } from 'core/dto'; + +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Permissions(PermissionsEnum.INTEGRATION_VIEW) +@Controller(':integrationId') +export class GitHubIntegrationController { + private readonly logger = new Logger('GitHubIntegrationController'); + + constructor( + private readonly _octokitService: OctokitService + ) { } + + /** + * Get GitHub installation metadata for a specific integration. + * + * This endpoint allows you to retrieve metadata associated with a GitHub installation for a given integration. + * + * @param {Request} request - The HTTP request object. + * @param {TenantOrganizationBaseDTO} query - Query parameters, including organizationId. + * @returns {Promise | void>} A promise that resolves with the GitHub installation metadata. + * @throws {HttpException} If the query parameters are invalid or if there's an error retrieving the metadata. + */ + @Get('/metadata') + @UsePipes(new ValidationPipe({ transform: true })) + async getGithubInstallationMetadata( + @Req() request: Request, + @Query() query: TenantOrganizationBaseDTO, + ): Promise | void> { + try { + // Validate the input data (You can use class-validator for validation) + if (!query || !query.organizationId) { + throw new HttpException('Invalid query parameter', HttpStatus.BAD_REQUEST); + } + + // Check if the request contains integration settings + const settings = request['integration']?.settings; + if (!settings || !settings.installation_id) { + throw new HttpException('Invalid request parameter: Missing or unauthorized integration', HttpStatus.UNAUTHORIZED); + } + + const installation_id = request['integration']['settings']['installation_id']; + if (installation_id) { + // Get installation metadata + const metadata = await this._octokitService.getGithubInstallationMetadata(installation_id); + return metadata.data; + } + + throw new HttpException('Invalid query parameter', HttpStatus.BAD_REQUEST); + } catch (error) { + // Handle errors and return an appropriate error response + this.logger.error('Error while retrieve github installation metadata', error.message); + throw new HttpException(`Error while retrieve github installation metadata: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Get GitHub repositories associated with a specific GitHub App installation within a given organization. + * + * This endpoint allows you to retrieve a list of GitHub repositories associated with a GitHub App installation within a specific organization. + * + * @param {Request} request - The HTTP request object. + * @param {TenantOrganizationBaseDTO} query - Query parameters containing organization information. + * @returns {Promise>} A promise that resolves with the GitHub repositories. + * @throws {HttpException} If the query parameters are invalid or if there's an error retrieving the repositories. + */ + @Get('/repositories') + @UsePipes(new ValidationPipe({ transform: true })) + async getGithubRepositories( + @Req() request: Request, + @Query() query: TenantOrganizationBaseDTO, + ): Promise | void> { + try { + // Validate the input data (You can use class-validator for validation) + if (!query || !query.organizationId) { + throw new HttpException('Invalid query parameter', HttpStatus.BAD_REQUEST); + } + + // Check if the request contains integration settings + const settings = request['integration']?.settings; + if (!settings || !settings.installation_id) { + throw new HttpException('Invalid request parameter: Missing or unauthorized integration', HttpStatus.UNAUTHORIZED); + } + + const installation_id = request['integration']['settings']['installation_id']; + if (installation_id) { + // Get installation repositories + const repositories = await this._octokitService.getGithubRepositories(installation_id); + return repositories.data; + } + + throw new HttpException('Invalid request parameter', HttpStatus.UNAUTHORIZED); + } catch (error) { + // Handle errors and return an appropriate error response + this.logger.error('Error while retrieving GitHub installation repositories', error.message); + throw new HttpException(`Error while retrieving GitHub installation repositories: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Get GitHub repository issues for a specific GitHub App installation within a given organization, owner, and repository. + * + * This endpoint allows you to retrieve issues associated with a GitHub repository for a GitHub App installation within a specific organization. + * + * @param {Request} request - The HTTP request object. + * @param {TenantOrganizationBaseDTO} query - Query parameters containing organization information. + * @param {string} owner - The owner (username or organization) of the repository. + * @param {string} repo - The name of the repository. + * @returns {Promise>} A promise that resolves with the GitHub repository issues. + * @throws {HttpException} If the query parameters are invalid or if there's an error retrieving the issues. + */ + @Get('/:owner/:repo/issues') + @UsePipes(new ValidationPipe({ transform: true })) + async getGithubRepositoryIssues( + @Req() request: Request, + @Query() query: TenantOrganizationBaseDTO, + @Param('owner') owner: string, + @Param('repo') repo: string, + ): Promise | void> { + try { + // Validate the input data (You can use class-validator for validation) + if (!query || !query.organizationId) { + throw new HttpException('Invalid query parameter', HttpStatus.BAD_REQUEST); + } + + // Check if the request contains integration settings + const settings = request['integration']?.settings; + if (!settings || !settings.installation_id) { + throw new HttpException('Invalid request parameter: Missing or unauthorized integration', HttpStatus.UNAUTHORIZED); + } + + const installation_id = request['integration']['settings']['installation_id']; + if (installation_id) { + // Get installation repositories + const issues = await this._octokitService.getGithubRepositoryIssues(installation_id, { owner, repo }); + return issues.data; + } + + throw new HttpException('Invalid request parameter', HttpStatus.UNAUTHORIZED); + } catch (error) { + // Handle errors and return an appropriate error response + this.logger.error('Error while retrieving GitHub installation repository issues', error.message); + throw new HttpException(`Error while retrieving GitHub installation repository issues: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/packages/core/src/integration/github/github.config.ts b/packages/core/src/integration/github/github.config.ts new file mode 100644 index 00000000000..657b558e652 --- /dev/null +++ b/packages/core/src/integration/github/github.config.ts @@ -0,0 +1 @@ +export const GITHUB_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'; diff --git a/packages/core/src/integration/github/github.controller.ts b/packages/core/src/integration/github/github.controller.ts new file mode 100644 index 00000000000..ebf901880a7 --- /dev/null +++ b/packages/core/src/integration/github/github.controller.ts @@ -0,0 +1,61 @@ +import { Controller, Post, Body, UseGuards, HttpException, HttpStatus, HttpCode, UsePipes, ValidationPipe } from '@nestjs/common'; +import { IGithubAppInstallInput, PermissionsEnum } from '@gauzy/contracts'; +import { PermissionGuard, TenantPermissionGuard } from 'shared/guards'; +import { Permissions } from 'shared/decorators'; +import { GithubService } from './github.service'; +import { GithubAppInstallDTO } from './dto'; + +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Permissions(PermissionsEnum.INTEGRATION_VIEW) +@Controller() +export class GitHubController { + constructor( + private readonly _githubService: GithubService + ) { } + + /** + * + * @param body + * @returns + */ + @Post('/install') + @HttpCode(HttpStatus.CREATED) + @UsePipes(new ValidationPipe()) + async addGithubAppInstallation( + @Body() input: GithubAppInstallDTO + ) { + try { + // Validate the input data (You can use class-validator for validation) + if (!input || !input.installation_id || !input.setup_action) { + throw new HttpException('Invalid input data', HttpStatus.BAD_REQUEST); + } + + // Add the GitHub installation using the service + return await this._githubService.addGithubAppInstallation(input); + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to add GitHub integration: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * + * @param body + * @returns + */ + @Post('/oauth') + @HttpCode(HttpStatus.CREATED) + // ToDo - Create Class Validation DTO to validate request + async oAuthEndpointAuthorization(@Body() input: IGithubAppInstallInput) { + try { + // Validate the input data (You can use class-validator for validation) + if (!input || !input.code) { + throw new HttpException('Invalid input data', HttpStatus.BAD_REQUEST); + } + return await this._githubService.oAuthEndpointAuthorization(input); + } catch (error) { + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to add GitHub integration: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/packages/core/src/integration/github/github.hooks.controller.ts b/packages/core/src/integration/github/github.hooks.controller.ts new file mode 100644 index 00000000000..ffb25ddfd32 --- /dev/null +++ b/packages/core/src/integration/github/github.hooks.controller.ts @@ -0,0 +1,31 @@ +import { Controller } from '@nestjs/common'; +import { Context } from 'probot'; +import { Public } from '@gauzy/common'; +import { Hook } from '@gauzy/integration-github'; +import { GithubHooksService } from './github.hooks.service'; + +@Public() +@Controller('webhook') +export class GitHubHooksController { + constructor( + private readonly _githubHooksService: GithubHooksService + ) { } + + /** + * + * @param context + */ + @Hook(['issues.opened']) + async issuesOpened(context: Context) { + await this._githubHooksService.issuesOpened(context); + } + + /** + * + * @param context + */ + @Hook(['issues.edited']) + async issuesEdited(context: Context) { + await this._githubHooksService.issuesEdited(context); + } +} diff --git a/packages/core/src/integration/github/github.hooks.service.ts b/packages/core/src/integration/github/github.hooks.service.ts new file mode 100644 index 00000000000..88c940ef696 --- /dev/null +++ b/packages/core/src/integration/github/github.hooks.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { Context } from 'probot'; + +@Injectable() +export class GithubHooksService { + + constructor() { } + + async issuesOpened(context: Context) { + console.log('Issue Created: ', context.payload); + } + + async issuesEdited(context: Context) { + console.log('Issue Edited', context.payload); + } +} diff --git a/packages/core/src/integration/github/github.middleware.ts b/packages/core/src/integration/github/github.middleware.ts new file mode 100644 index 00000000000..0e1412964a1 --- /dev/null +++ b/packages/core/src/integration/github/github.middleware.ts @@ -0,0 +1,54 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { isNotEmpty } from '@gauzy/common'; +import { IntegrationEnum } from '@gauzy/contracts'; +import { arrayToObject } from 'core/utils'; +import { IntegrationTenantService } from 'integration-tenant/integration-tenant.service'; + +@Injectable() +export class GithubMiddleware implements NestMiddleware { + + constructor( + private readonly _integrationTenantService: IntegrationTenantService, + ) { } + + async use( + request: Request, + response: Response, + next: NextFunction + ) { + try { + const integrationId = request.params['integrationId']; + const queryParameters = request.query; + + const tenantId = queryParameters.tenantId ? queryParameters.tenantId.toString() : request.header('Tenant-Id'); + const organizationId = queryParameters.organizationId ? queryParameters.organizationId.toString() : request.header('Organization-Id'); + + // Check if tenant and organization IDs are not empty + if (isNotEmpty(tenantId) && isNotEmpty(organizationId)) { + // Fetch integration settings from the service + const { settings = [] } = await this._integrationTenantService.findOneByIdString(integrationId, { + where: { + tenantId, + organizationId + }, + relations: { + settings: true + } + }); + /** */ + request['integration'] = new Object({ + name: IntegrationEnum.GITHUB, + // Convert settings array to an object + settings: arrayToObject(settings, 'settingsName', 'settingsValue') + }); + } + } catch (error) { + console.log(`Error while getting integration (${IntegrationEnum.GITHUB}) tenant inside middleware: %s`, error?.message); + console.log(request.path, request.url); + } + + // Continue to the next middleware or route handler + next(); + } +} diff --git a/packages/core/src/integration/github/github.module.ts b/packages/core/src/integration/github/github.module.ts new file mode 100644 index 00000000000..d7e235b9d18 --- /dev/null +++ b/packages/core/src/integration/github/github.module.ts @@ -0,0 +1,63 @@ +import { MiddlewareConsumer, Module, NestModule, RequestMethod, forwardRef } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { CqrsModule } from '@nestjs/cqrs'; +import { TenantModule } from 'tenant/tenant.module'; +import { UserModule } from 'user/user.module'; +import { IntegrationModule } from 'integration/integration.module'; +import { IntegrationTenantModule } from 'integration-tenant/integration-tenant.module'; +import { GitHubAuthorizationController } from './github-authorization.controller'; +import { GitHubIntegrationController } from './github-integration.controller'; +import { GitHubController } from './github.controller'; +import { GithubService } from './github.service'; +import { GithubMiddleware } from './github.middleware'; +import { GitHubHooksController } from './github.hooks.controller'; +import { GithubHooksService } from './github.hooks.service'; + +@Module({ + imports: [ + HttpModule, + TenantModule, + UserModule, + CqrsModule, + forwardRef(() => IntegrationModule), + forwardRef(() => IntegrationTenantModule) + ], + controllers: [ + GitHubAuthorizationController, + GitHubController, + GitHubHooksController, + GitHubIntegrationController, + ], + providers: [ + GithubService, + GithubHooksService, + // Define middleware heres + GithubMiddleware + ], + exports: [], +}) +export class GithubModule implements NestModule { + /** + * + * @param consumer + */ + configure(consumer: MiddlewareConsumer) { + // Apply middlewares to specific controllers + consumer + .apply(GithubMiddleware) + .forRoutes( + { + path: '/integration/github/:integrationId/metadata', + method: RequestMethod.GET + }, + { + path: '/integration/github/:integrationId/repositories', + method: RequestMethod.GET + }, + { + path: '/integration/github/:integrationId/:owner/:repo/issues', + method: RequestMethod.GET + } + ); // Apply to specific routes and methods + } +} diff --git a/packages/core/src/integration/github/github.service.ts b/packages/core/src/integration/github/github.service.ts new file mode 100644 index 00000000000..a6bbd32af1f --- /dev/null +++ b/packages/core/src/integration/github/github.service.ts @@ -0,0 +1,170 @@ +import { BadRequestException, HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { HttpService } from '@nestjs/axios'; +import { catchError, lastValueFrom, switchMap } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { environment } from '@gauzy/config'; +import { IGithubAppInstallInput, IIntegrationTenant, IntegrationEnum } from '@gauzy/contracts'; +import { IntegrationTenantFirstOrCreateCommand } from 'integration-tenant/commands'; +import { IntegrationService } from 'integration/integration.service'; +import { RequestContext } from '../../core/context'; +import { GITHUB_ACCESS_TOKEN_URL } from './github.config'; +const { github } = environment; + +@Injectable() +export class GithubService { + private readonly logger = new Logger('GithubService'); + + constructor( + private readonly _httpService: HttpService, + private readonly _commandBus: CommandBus, + private readonly _integrationService: IntegrationService + ) { } + + async openIssue({ title, body, owner, repo, installationI }) { + console.log({ title, body, owner, repo, installationI }); + } + + async editIssue({ issueNumber, title, body, owner, repo, installationI }) { + console.log({ issueNumber, title, body, owner, repo, installationI }); + } + + /** + * + * @param input + * @returns + */ + async addGithubAppInstallation(input: IGithubAppInstallInput) { + try { + // Validate the input data (You can use class-validator for validation) + if (!input || !input.installation_id || !input.setup_action) { + throw new HttpException('Invalid input data', HttpStatus.BAD_REQUEST); + } + + const tenantId = RequestContext.currentTenantId() || input.tenantId; + const { installation_id, setup_action, organizationId } = input; + + /** Find the GitHub integration */ + const integration = await this._integrationService.findOneByOptions({ + where: { + provider: IntegrationEnum.GITHUB + } + }); + + /** Execute the command to create the integration tenant settings */ + return await this._commandBus.execute( + new IntegrationTenantFirstOrCreateCommand({ + name: IntegrationEnum.GITHUB, + integration: { + provider: IntegrationEnum.GITHUB + }, + tenantId, + organizationId, + }, { + name: IntegrationEnum.GITHUB, + integration, + tenantId, + organizationId, + entitySettings: [], + settings: [ + { + settingsName: 'installation_id', + settingsValue: installation_id + }, + { + settingsName: 'setup_action', + settingsValue: setup_action + }, + ].map((setting) => ({ + ...setting, + tenantId, + organizationId, + })) + }) + ); + } catch (error) { + this.logger.error(`Error while creating ${IntegrationEnum.GAUZY_AI} integration settings`, error?.message); + throw new Error(`Failed to add ${IntegrationEnum.GAUZY_AI} App Installation`); + } + } + + /** + * + * @param input + * @returns + */ + async oAuthEndpointAuthorization(input: IGithubAppInstallInput): Promise { + try { + // Validate the input data (You can use class-validator for validation) + if (!input || !input.code) { + throw new HttpException('Invalid input data', HttpStatus.BAD_REQUEST); + } + + const tenantId = RequestContext.currentTenantId() || input.tenantId; + const { code, organizationId } = input; + + const integration = await this._integrationService.findOneByOptions({ + where: { + name: IntegrationEnum.GITHUB + } + }); + + const urlParams = new URLSearchParams(); + urlParams.append('client_id', github.clientId); + urlParams.append('client_secret', github.clientSecret); + urlParams.append('code', code); + + const tokens$ = this._httpService.post(GITHUB_ACCESS_TOKEN_URL, urlParams, { + headers: { + 'accept': 'application/json' + } + }).pipe( + filter(({ data }) => !!data.error), + switchMap(({ data }) => this._commandBus.execute( + new IntegrationTenantFirstOrCreateCommand( + { + name: IntegrationEnum.GITHUB, + integration: { + provider: IntegrationEnum.GITHUB + }, + tenantId, + organizationId, + }, + { + name: IntegrationEnum.GITHUB, + integration, + tenantId, + organizationId, + entitySettings: [], + settings: [ + { + settingsName: 'token_type', + settingsValue: data.token_type + }, + { + settingsName: 'access_token', + settingsValue: data.access_token + }, + { + settingsName: 'scope', + settingsValue: data.scope + } + ].map((setting) => ({ + ...setting, + tenantId, + organizationId, + })) + } + ) + )), + catchError((error) => { + throw new BadRequestException(error); + }) + ); + return await lastValueFrom(tokens$); + } catch (error) { + this.logger.error('Error while creating GitHub integration settings', error?.message); + throw new Error('Failed to add GitHub App Installation'); + } + } +} diff --git a/packages/core/src/integration/hubstaff/hubstaff-authorization.controller.ts b/packages/core/src/integration/hubstaff/hubstaff-authorization.controller.ts index 7619ac0e7d4..bc8d43cde6f 100644 --- a/packages/core/src/integration/hubstaff/hubstaff-authorization.controller.ts +++ b/packages/core/src/integration/hubstaff/hubstaff-authorization.controller.ts @@ -1,9 +1,12 @@ -import { Controller, Get, Query, Res } from '@nestjs/common'; +import { Controller, Get, HttpException, HttpStatus, Query, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { Response } from 'express'; import { ConfigService } from '@gauzy/config'; -import { Public } from '@gauzy/common'; +import { IHubstaffConfig, Public } from '@gauzy/common'; +import { IntegrationEnum } from '@gauzy/contracts'; @ApiTags('Hubstaff Integrations') +@Public() @Controller() export class HubstaffAuthorizationController { constructor( @@ -11,27 +14,35 @@ export class HubstaffAuthorizationController { ) { } /** - * Hubstaff Integration Authorization Flow Callback - * - * @param code - * @param state - * @param res - * @returns - */ - @Public() + * Handle the callback from the Hubstaff integration. + * + * @param {any} query - The query parameters from the callback. + * @param {Response} response - Express Response object. + */ @Get('callback') - async hubstaffCallback( - @Query('code') code: string, - @Query('state') state: string, - @Res() res: any + async hubstaffIntegrationCallback( + @Query() query: any, + @Res() response: Response ) { try { - if (code && state) { - return res.redirect(`${this._config.get('clientBaseUrl')}/#/pages/integrations/hubstaff?code=${code}&state=${state}`); + // Validate the input data (You can use class-validator for validation) + if (!query || !query.code || !query.state) { + throw new HttpException('Invalid query parameters', HttpStatus.BAD_REQUEST); } - return res.redirect(`${this._config.get('clientBaseUrl')}/#/pages/integrations/hubstaff`); + + /** Hubstaff Config Options */ + const hubstaff = this._config.get('hubstaff') as IHubstaffConfig; + + /** Construct the redirect URL with query parameters */ + const urlParams = new URLSearchParams(); + urlParams.append('code', query.code); + urlParams.append('state', query.state); + + /** Redirect to the URL */ + return response.redirect(`${hubstaff.postInstallUrl}?${urlParams.toString()}`); } catch (error) { - return res.redirect(`${this._config.get('clientBaseUrl')}/#/pages/integrations/hubstaff`); + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to add ${IntegrationEnum.HUBSTAFF} integration: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); } } } diff --git a/packages/core/src/integration/hubstaff/hubstaff.module.ts b/packages/core/src/integration/hubstaff/hubstaff.module.ts index c64df3b9c92..6282e9501b5 100644 --- a/packages/core/src/integration/hubstaff/hubstaff.module.ts +++ b/packages/core/src/integration/hubstaff/hubstaff.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { CqrsModule } from '@nestjs/cqrs'; import { HUBSTAFF_API_URL } from '@gauzy/integration-hubstaff'; @@ -9,6 +9,7 @@ import { TenantModule } from 'tenant/tenant.module'; import { OrganizationModule } from 'organization/organization.module'; import { IntegrationEntitySettingModule } from 'integration-entity-setting/integration-entity-setting.module'; import { IntegrationEntitySettingTiedModule } from 'integration-entity-setting-tied/integration-entity-setting-tied.module'; +import { IntegrationModule } from 'integration/integration.module'; import { IntegrationMapModule } from 'integration-map/integration-map.module'; import { IntegrationTenantModule } from 'integration-tenant/integration-tenant.module'; import { IntegrationSettingModule } from 'integration-setting/integration-setting.module'; @@ -27,6 +28,7 @@ import { HubstaffAuthorizationController } from './hubstaff-authorization.contro UserModule, OrganizationModule, OrganizationProjectModule, + forwardRef(() => IntegrationModule), IntegrationTenantModule, IntegrationSettingModule, IntegrationEntitySettingModule, diff --git a/packages/core/src/integration/hubstaff/hubstaff.service.ts b/packages/core/src/integration/hubstaff/hubstaff.service.ts index 13c5030ac3c..fe4d55c8690 100644 --- a/packages/core/src/integration/hubstaff/hubstaff.service.ts +++ b/packages/core/src/integration/hubstaff/hubstaff.service.ts @@ -67,7 +67,8 @@ import { IntegrationMapSyncTimeSlotCommand } from 'integration-map/commands'; import { IntegrationTenantService } from 'integration-tenant/integration-tenant.service'; -import { IntegrationTenantCreateCommand } from 'integration-tenant/commands'; +import { IntegrationTenantFirstOrCreateCommand } from 'integration-tenant/commands'; +import { IntegrationService } from 'integration/integration.service'; @Injectable() export class HubstaffService { @@ -79,7 +80,8 @@ export class HubstaffService { private readonly _roleService: RoleService, private readonly _organizationService: OrganizationService, private readonly _userService: UserService, - private readonly _commandBus: CommandBus + private readonly _commandBus: CommandBus, + private readonly _integrationService: IntegrationService ) { } async fetchIntegration(url: string, token: string): Promise { @@ -189,7 +191,6 @@ export class HubstaffService { async addIntegration( body: ICreateHubstaffIntegrationInput ): Promise { - const tenantId = RequestContext.currentTenantId(); const { client_id, client_secret, code, redirect_uri, organizationId } = body; @@ -221,16 +222,31 @@ export class HubstaffService { : settingEntity ) as IIntegrationEntitySetting[]; + /** */ + const integration = await this._integrationService.findOneByOptions({ + where: { + provider: IntegrationEnum.HUBSTAFF + } + }); + const tokens$ = this._httpService.post(`${HUBSTAFF_AUTHORIZATION_URL}/access_tokens`, urlParams, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).pipe( switchMap(({ data }) => this._commandBus.execute( - new IntegrationTenantCreateCommand({ - organizationId, + new IntegrationTenantFirstOrCreateCommand({ + name: IntegrationEnum.HUBSTAFF, + integration: { + provider: IntegrationEnum.HUBSTAFF + }, tenantId, + organizationId, + }, { name: IntegrationEnum.HUBSTAFF, + integration, + organizationId, + tenantId, entitySettings: entitySettings, settings: [ { @@ -249,7 +265,11 @@ export class HubstaffService { settingsName: 'refresh_token', settingsValue: data.refresh_token } - ] + ].map((setting) => ({ + ...setting, + tenantId, + organizationId, + })) }) )), catchError((err) => { diff --git a/packages/core/src/integration/integration-type.entity.ts b/packages/core/src/integration/integration-type.entity.ts index 9abae870f9d..9866c79617d 100644 --- a/packages/core/src/integration/integration-type.entity.ts +++ b/packages/core/src/integration/integration-type.entity.ts @@ -1,10 +1,11 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity, ManyToMany } from 'typeorm'; -import { IsNotEmpty, IsNumber } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Column, Entity, ManyToMany, Unique } from 'typeorm'; +import { IsNotEmpty, IsNumber, IsOptional } from 'class-validator'; import { IIntegration, IIntegrationType } from '@gauzy/contracts'; import { BaseEntity, Integration } from '../core/entities/internal'; @Entity('integration_type') +@Unique(['name']) export class IntegrationType extends BaseEntity implements IIntegrationType { @ApiProperty({ type: () => String }) @@ -12,6 +13,16 @@ export class IntegrationType extends BaseEntity implements IIntegrationType { @Column() name: string; + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @Column({ nullable: true }) + description: string; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @Column({ nullable: true }) + icon: string; + @ApiProperty({ type: () => String }) @IsNotEmpty() @Column() @@ -32,5 +43,5 @@ export class IntegrationType extends BaseEntity implements IIntegrationType { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) - integrations?: IIntegration[]; + integrations: IIntegration[]; } diff --git a/packages/core/src/integration/integration-type.seed.ts b/packages/core/src/integration/integration-type.seed.ts index f6563e1e910..77192e2230e 100644 --- a/packages/core/src/integration/integration-type.seed.ts +++ b/packages/core/src/integration/integration-type.seed.ts @@ -1,65 +1,25 @@ import { DataSource } from 'typeorm'; import { IntegrationType } from './integration-type.entity'; -import { - IntegrationTypeGroupEnum, - IntegrationTypeNameEnum -} from '@gauzy/contracts'; - -const DEFAULT_INTEGRATION_TYPES = [ - { - name: IntegrationTypeNameEnum.ALL_INTEGRATIONS, - groupName: IntegrationTypeGroupEnum.FEATURED, - order: 1 - }, - { - name: IntegrationTypeNameEnum.FOR_SALES_TEAMS, - groupName: IntegrationTypeGroupEnum.FEATURED, - order: 1 - }, - { - name: IntegrationTypeNameEnum.FOR_ACCOUNTANTS, - groupName: IntegrationTypeGroupEnum.FEATURED, - order: 1 - }, - { - name: IntegrationTypeNameEnum.FOR_SUPPORT_TEAMS, - groupName: IntegrationTypeGroupEnum.FEATURED, - order: 1 - }, - { - name: IntegrationTypeNameEnum.CRM, - groupName: IntegrationTypeGroupEnum.CATEGORIES, - order: 2 - }, - { - name: IntegrationTypeNameEnum.SCHEDULING, - groupName: IntegrationTypeGroupEnum.CATEGORIES, - order: 2 - }, - { - name: IntegrationTypeNameEnum.TOOLS, - groupName: IntegrationTypeGroupEnum.CATEGORIES, - order: 2 - } -]; +import { DEFAULT_INTEGRATION_TYPES } from './default-integration-type'; export const createDefaultIntegrationTypes = async ( dataSource: DataSource ): Promise => { - const integrationTypes = DEFAULT_INTEGRATION_TYPES.map( - ({ name, groupName, order }) => { - const entity = new IntegrationType(); - entity.name = name; - entity.groupName = groupName; - entity.order = order; - return entity; - } - ); + const integrationTypes = DEFAULT_INTEGRATION_TYPES.map(({ name, groupName, order, icon, description }) => { + const entity = new IntegrationType(); + entity.name = name; + entity.groupName = groupName; + entity.order = order; + entity.icon = icon; + entity.description = description; + return entity; + }); return await insertIntegrationTypes(dataSource, integrationTypes); }; const insertIntegrationTypes = async ( dataSource: DataSource, integrationTypes: IntegrationType[] -): Promise => - await dataSource.manager.save(integrationTypes); +): Promise => { + return await dataSource.manager.save(integrationTypes); +} diff --git a/packages/core/src/integration/integration.entity.ts b/packages/core/src/integration/integration.entity.ts index 790a29c94bb..fa0a9197ae5 100644 --- a/packages/core/src/integration/integration.entity.ts +++ b/packages/core/src/integration/integration.entity.ts @@ -15,6 +15,16 @@ export class Integration extends BaseEntity implements IIntegration { @Column() // Define a unique constraint on the "name" column name: string; + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @Column({ nullable: true }) // Define a unique constraint on the "provider" column (E.g github, jira, hubstaff) + provider: string; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @Column({ nullable: true }) + redirectUrl: string; + @ApiPropertyOptional({ type: () => String }) @IsOptional() @Column({ nullable: true }) @@ -24,34 +34,29 @@ export class Integration extends BaseEntity implements IIntegration { @IsOptional() @IsBoolean() @Column({ default: false }) - isComingSoon?: boolean; + isComingSoon: boolean; @ApiPropertyOptional({ type: () => Boolean, default: false }) @IsOptional() @IsBoolean() @Column({ default: false }) - isPaid?: boolean; - - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @Column({ nullable: true }) - version?: string; + isPaid: boolean; @ApiPropertyOptional({ type: () => String }) @IsOptional() @Column({ nullable: true }) - docUrl?: string; + version: string; @ApiPropertyOptional({ type: () => String }) @IsOptional() @Column({ nullable: true }) - navigationUrl?: string; + docUrl: string; @ApiPropertyOptional({ type: () => Boolean, default: false }) @IsOptional() @IsBoolean() @Column({ default: false }) - isFreeTrial?: boolean; + isFreeTrial: boolean; @ApiPropertyOptional({ type: () => Number }) @IsOptional() @@ -62,16 +67,15 @@ export class Integration extends BaseEntity implements IIntegration { type: 'numeric', transformer: new ColumnNumericTransformerPipe() }) - freeTrialPeriod?: number; + freeTrialPeriod: number; @ApiPropertyOptional({ type: () => Number }) @IsOptional() @IsNumber() @Column({ nullable: true }) - order?: number; + order: number; fullImgUrl?: string; - /* |-------------------------------------------------------------------------- | @ManyToMany diff --git a/packages/core/src/integration/integration.module.ts b/packages/core/src/integration/integration.module.ts index b276b1a04ee..01ee1cf0432 100644 --- a/packages/core/src/integration/integration.module.ts +++ b/packages/core/src/integration/integration.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RouterModule } from 'nest-router'; import { CqrsModule } from '@nestjs/cqrs'; @@ -11,6 +11,8 @@ import { IntegrationService } from './integration.service'; import { IntegrationController } from './integration.controller'; import { CommandHandlers } from './commands/handlers'; import { IntegrationTenantModule } from '../integration-tenant/integration-tenant.module'; +import { GithubModule } from './github/github.module'; +import { IntegrationAIModule } from './gauzy-ai/integration-ai.module'; @Module({ imports: [ @@ -19,6 +21,8 @@ import { IntegrationTenantModule } from '../integration-tenant/integration-tenan path: '/integration', module: IntegrationModule, children: [ { path: '/hubstaff', module: HubstaffModule }, + { path: '/github', module: GithubModule }, + { path: '/gauzy-ai', module: IntegrationAIModule }, { path: '/', module: IntegrationModule } ] }, @@ -31,7 +35,9 @@ import { IntegrationTenantModule } from '../integration-tenant/integration-tenan IntegrationTenantModule, TenantModule, UserModule, - HubstaffModule, + forwardRef(() => GithubModule), + forwardRef(() => HubstaffModule), + forwardRef(() => IntegrationAIModule), CqrsModule ], controllers: [ diff --git a/packages/core/src/integration/integration.seed.ts b/packages/core/src/integration/integration.seed.ts index ab19d5658b2..43fa9fff248 100644 --- a/packages/core/src/integration/integration.seed.ts +++ b/packages/core/src/integration/integration.seed.ts @@ -1,8 +1,9 @@ import { DataSource } from 'typeorm'; import { getConfig } from '@gauzy/config'; -import { IIntegration, DEFAULT_INTEGRATIONS, IIntegrationType } from '@gauzy/contracts'; +import { IIntegration, IIntegrationType } from '@gauzy/contracts'; import { cleanAssets, copyAssets } from './../core/seeds/utils'; import { Integration } from './integration.entity'; +import { DEFAULT_INTEGRATIONS } from './default-integration'; const config = getConfig(); @@ -22,14 +23,15 @@ export const createDefaultIntegrations = async ( const integrations: IIntegration[] = []; for await (const integration of DEFAULT_INTEGRATIONS) { - const { name, imgSrc, isComingSoon, integrationTypesMap, order, navigationUrl } = integration; + const { name, imgSrc, isComingSoon, integrationTypesMap, order, provider, redirectUrl } = integration; const entity = new Integration(); entity.name = name; entity.imgSrc = copyAssets(imgSrc, config, destDir); entity.isComingSoon = isComingSoon; entity.order = order; - entity.navigationUrl = navigationUrl; + entity.redirectUrl = redirectUrl; + entity.provider = provider; entity.integrationTypes = integrationTypes.filter((it) => integrationTypesMap.includes(it.name) ); diff --git a/packages/core/src/integration/utils.ts b/packages/core/src/integration/utils.ts new file mode 100644 index 00000000000..2450e65436b --- /dev/null +++ b/packages/core/src/integration/utils.ts @@ -0,0 +1,183 @@ + +import { QueryRunner } from "typeorm"; +import { v4 as uuidv4 } from 'uuid'; +import { getConfig } from "@gauzy/config"; +import { IIntegration, IIntegrationType, IntegrationTypeEnum } from "@gauzy/contracts"; +import { copyAssets } from "./../core/seeds/utils"; +import { DEFAULT_INTEGRATION_TYPES } from "./default-integration-type"; + +export class IntegrationsUtils { + /** + * + * @param queryRunner + */ + public static async upsertIntegrationsAndIntegrationTypes(queryRunner: QueryRunner, integrations: any[]): Promise { + const destDir = 'integrations'; + for await (const { name, imgSrc, isComingSoon, order, redirectUrl, provider, integrationTypesMap } of integrations) { + try { + const filepath = `integrations/${imgSrc}`; + + let upsertQuery = ``; + const payload = [name, filepath, isComingSoon, order, redirectUrl, provider]; + + if (queryRunner.connection.options.type === 'sqlite') { + // For SQLite, manually generate a UUID using uuidv4() + const generatedId = uuidv4(); payload.push(generatedId); + upsertQuery = ` + INSERT INTO integration ( + "name", "imgSrc", "isComingSoon", "order", "redirectUrl", "provider", "id" + ) + VALUES ( + $1, $2, $3, $4, $5, $6, $7 + ) + ON CONFLICT(name) DO UPDATE + SET + "imgSrc" = $2, + "isComingSoon" = $3, + "order" = $4, + "redirectUrl" = $5, + "provider" = $6 + RETURNING id; + `; + } else { + upsertQuery = ` + INSERT INTO "integration" ( + "name", "imgSrc", "isComingSoon", "order", "redirectUrl", "provider" + ) + VALUES ( + $1, $2, $3, $4, $5, $6 + ) + ON CONFLICT(name) DO UPDATE + SET + "imgSrc" = $2, + "isComingSoon" = $3, + "order" = $4, + "redirectUrl" = $5, + "provider" = $6 + RETURNING id; + `; + } + const [integration] = await queryRunner.query(upsertQuery, payload); + + // Step 3: Insert entry in join table to associate Integration with IntegrationType + await IntegrationsUtils.syncIntegrationType( + queryRunner, + integration, + await this.getIntegrationTypeByName(queryRunner, integrationTypesMap) + ); + + copyAssets(imgSrc, getConfig(), destDir); + } catch (error) { + // since we have errors let's rollback changes we made + console.log(`Error while updating integration: (${name}) in production server`, error); + } + } + } + + /** + * + * @param queryRunner + * @param integrationTypesMap + * @returns + */ + public static async getIntegrationTypeByName( + queryRunner: QueryRunner, + integrationTypeNames: any[] + ): Promise { + try { + return await queryRunner.query(`SELECT * FROM "integration_type" WHERE "integration_type"."name" IN ('${integrationTypeNames.join("','")}')`); + } catch (error) { + console.log('Error while querying integration types:', error); + return []; + } + } + + /** + * + * @param queryRunner + * @param integrationTypeName + */ + public static async upsertIntegrationTypes( + queryRunner: QueryRunner, + integrationTypeNames: IntegrationTypeEnum[] + ) { + for await (const integrationTypeName of integrationTypeNames) { + const { name, description, icon, groupName, order } = DEFAULT_INTEGRATION_TYPES.find( + (type) => type.name === integrationTypeName + ); + const payload = [name, description, icon, groupName, order]; + + let upsertQuery = ``; + if (queryRunner.connection.options.type === 'sqlite') { + // For SQLite, manually generate a UUID using uuidv4() + upsertQuery = ` + INSERT INTO "integration_type" ( + "name", "description", "icon", "groupName", "order", "id" + ) + VALUES ( + $1, $2, $3, $4, $5, $6 + ) + ON CONFLICT(name) DO UPDATE + SET + "description" = $2, + "icon" = $3, + "groupName" = $4, + "order" = $5 + RETURNING id; + `; + } else { + upsertQuery = ` + INSERT INTO "integration_type" ( + "name", "description", "icon", "groupName", "order" + ) + VALUES ( + $1, $2, $3, $4, $5 + ) + ON CONFLICT(name) DO UPDATE + SET + "description" = $2, + "icon" = $3, + "groupName" = $4, + "order" = $5 + RETURNING id; + `; + } + await queryRunner.query(upsertQuery, payload); + } + } + + /** + * + * + * @param queryRunner + * @param integration + * @param integrationTypes + */ + public static async syncIntegrationType( + queryRunner: QueryRunner, + integration: IIntegration, + integrationTypes: IIntegrationType[] + ) { + if (integration) { + const integrationId = integration.id; + for await (const integrationType of integrationTypes) { + const insertPivotQuery = ` + INSERT INTO "integration_integration_type" ( + "integrationId", + "integrationTypeId" + ) + SELECT + $1, $2 + WHERE NOT EXISTS ( + SELECT 1 + FROM "integration_integration_type" + WHERE + "integrationId" = $1 AND + "integrationTypeId" = $2 + ) + `; + await queryRunner.query(insertPivotQuery, [integrationId, integrationType.id]); + } + } + } +} diff --git a/packages/core/src/public-share/team/public-team.service.ts b/packages/core/src/public-share/team/public-team.service.ts index 818bf1da1e7..6a455477e0e 100644 --- a/packages/core/src/public-share/team/public-team.service.ts +++ b/packages/core/src/public-share/team/public-team.service.ts @@ -2,7 +2,7 @@ import { IDateRangePicker, IOrganizationTeam, IOrganizationTeamEmployee, - IOrganizationTeamStatisticInput + IOrganizationTeamStatisticInput, } from '@gauzy/contracts'; import { Injectable, NotFoundException } from '@nestjs/common'; import { parseToBoolean } from '@gauzy/common'; @@ -14,14 +14,13 @@ import { TimerService } from './../../time-tracking/timer/timer.service'; @Injectable() export class PublicTeamService { - constructor( @InjectRepository(OrganizationTeam) private readonly repository: Repository, private readonly _statisticService: StatisticService, private readonly _timerService: TimerService - ) { } + ) {} /** * GET organization team by profile link @@ -40,7 +39,7 @@ export class PublicTeamService { organization: { id: true, name: true, - brandColor: true + brandColor: true, }, members: { id: true, @@ -53,24 +52,29 @@ export class PublicTeamService { id: true, firstName: true, lastName: true, - imageUrl: true - } - } - } + imageUrl: true, + }, + isActive: true, + isOnline: true, + }, + }, }, where: { public: true, - ...params + ...params, }, - ...( - (options.relations) ? { - relations: options.relations - } : {} - ), + ...(options.relations + ? { + relations: options.relations, + } + : {}), }); if ('members' in team) { const { members, organizationId, tenantId } = team; - team['members'] = await this.syncMembers({ organizationId, tenantId, members }, options); + team['members'] = await this.syncMembers( + { organizationId, tenantId, members }, + options + ); } return team; } catch (error) { @@ -85,55 +89,57 @@ export class PublicTeamService { * @returns */ async syncMembers( - { - organizationId, - tenantId, - members - }, + { organizationId, tenantId, members }, options: IDateRangePicker & IOrganizationTeamStatisticInput ): Promise { try { const { startDate, endDate, withLaskWorkedTask, source } = options; return await Promise.all( - await members.map( - async (member: IOrganizationTeamEmployee) => { - const { employeeId, organizationTeamId } = member; - const timerWorkedStatus = await this._timerService.getTimerWorkedStatus({ + await members.map(async (member: IOrganizationTeamEmployee) => { + const { employeeId, organizationTeamId } = member; + const timerWorkedStatus = + await this._timerService.getTimerWorkedStatus({ source, employeeId, organizationTeamId, organizationId, tenantId, - ...( - (parseToBoolean(withLaskWorkedTask)) ? { - relations: ['task'] - } : {} - ), + ...(parseToBoolean(withLaskWorkedTask) + ? { + relations: ['task'], + } + : {}), }); - return { - ...member, - lastWorkedTask: parseToBoolean(withLaskWorkedTask) ? timerWorkedStatus.lastLog?.task : null, - timerStatus: timerWorkedStatus?.timerStatus, - totalWorkedTasks: await this._statisticService.getTasks({ - organizationId, - tenantId, - organizationTeamId, - employeeIds: [employeeId] - }), - totalTodayTasks: await this._statisticService.getTasks({ + return { + ...member, + lastWorkedTask: parseToBoolean(withLaskWorkedTask) + ? timerWorkedStatus.lastLog?.task + : null, + timerStatus: timerWorkedStatus?.timerStatus, + totalWorkedTasks: await this._statisticService.getTasks( + { organizationId, tenantId, organizationTeamId, employeeIds: [employeeId], - startDate, - endDate - }), - } - } - ) + } + ), + totalTodayTasks: await this._statisticService.getTasks({ + organizationId, + tenantId, + organizationTeamId, + employeeIds: [employeeId], + startDate, + endDate, + }), + }; + }) ); } catch (error) { - console.log('Error while retrieving team members worked tasks', error); + console.log( + 'Error while retrieving team members worked tasks', + error + ); } } } diff --git a/packages/core/src/tasks/commands/handlers/task-create.handler.ts b/packages/core/src/tasks/commands/handlers/task-create.handler.ts index 2c0e7ed24c6..7f05fdb67ca 100644 --- a/packages/core/src/tasks/commands/handlers/task-create.handler.ts +++ b/packages/core/src/tasks/commands/handlers/task-create.handler.ts @@ -8,7 +8,6 @@ import { TaskService } from '../../task.service'; @CommandHandler(TaskCreateCommand) export class TaskCreateHandler implements ICommandHandler { - constructor( private readonly _taskService: TaskService, private readonly _organizationProjectService: OrganizationProjectService @@ -24,15 +23,17 @@ export class TaskCreateHandler implements ICommandHandler { /** If project found then use project name as a task prefix */ if (input.projectId) { const { projectId } = input; - project = await this._organizationProjectService.findOneByIdString(projectId); + project = await this._organizationProjectService.findOneByIdString( + projectId + ); } - const projectId = (project) ? project.id : null; - const taskPrefix = (project) ? project.name.substring(0, 3) : null; + const projectId = project ? project.id : null; + const taskPrefix = project ? project.name.substring(0, 3) : null; const maxNumber = await this._taskService.getMaxTaskNumberByProject({ organizationId, - projectId + projectId, }); return await this._taskService.create({ diff --git a/packages/core/src/tasks/commands/handlers/task-update.handler.ts b/packages/core/src/tasks/commands/handlers/task-update.handler.ts index d6bb6efdc9c..1b54b6e58ae 100644 --- a/packages/core/src/tasks/commands/handlers/task-update.handler.ts +++ b/packages/core/src/tasks/commands/handlers/task-update.handler.ts @@ -6,7 +6,6 @@ import { TaskUpdateCommand } from '../task-update.command'; @CommandHandler(TaskUpdateCommand) export class TaskUpdateHandler implements ICommandHandler { - constructor( private readonly _taskService: TaskService ) { } @@ -23,10 +22,7 @@ export class TaskUpdateHandler implements ICommandHandler { * @param request * @returns */ - public async update( - id: string, - request: ITaskUpdateInput - ): Promise { + public async update(id: string, request: ITaskUpdateInput): Promise { try { const task = await this._taskService.findOneByIdString(id); @@ -39,14 +35,15 @@ export class TaskUpdateHandler implements ICommandHandler { const { organizationId } = task; const maxNumber = await this._taskService.getMaxTaskNumberByProject({ organizationId, - projectId + projectId, }); await this._taskService.update(id, { projectId, - number: maxNumber + 1 + number: maxNumber + 1, }); } } + return await this._taskService.create({ ...request, id diff --git a/packages/core/src/tasks/linked-issue/dto/create-task-linked-issue.dto.ts b/packages/core/src/tasks/linked-issue/dto/create-task-linked-issue.dto.ts index 55e8223242a..63db69ab3d4 100644 --- a/packages/core/src/tasks/linked-issue/dto/create-task-linked-issue.dto.ts +++ b/packages/core/src/tasks/linked-issue/dto/create-task-linked-issue.dto.ts @@ -1,6 +1,4 @@ -import { ITaskLinkedIssue } from '@gauzy/contracts'; +import { ITaskLinkedIssueCreateInput } from '@gauzy/contracts'; import { TaskLinkedIssueDTO } from './task-linked-issue.dto'; -export class CreateTaskLinkedIssueDTO - extends TaskLinkedIssueDTO - implements ITaskLinkedIssue {} +export class CreateTaskLinkedIssueDTO extends TaskLinkedIssueDTO implements ITaskLinkedIssueCreateInput { } diff --git a/packages/core/src/tasks/linked-issue/dto/index.ts b/packages/core/src/tasks/linked-issue/dto/index.ts index e2d16606913..1bfb16afd92 100644 --- a/packages/core/src/tasks/linked-issue/dto/index.ts +++ b/packages/core/src/tasks/linked-issue/dto/index.ts @@ -1 +1,2 @@ export * from './create-task-linked-issue.dto'; +export * from './update-task-linked-issue.dto'; diff --git a/packages/core/src/tasks/linked-issue/dto/update-task-linked-issue.dto.ts b/packages/core/src/tasks/linked-issue/dto/update-task-linked-issue.dto.ts new file mode 100644 index 00000000000..8e0e5b23dd9 --- /dev/null +++ b/packages/core/src/tasks/linked-issue/dto/update-task-linked-issue.dto.ts @@ -0,0 +1,4 @@ +import { ITaskLinkedIssueUpdateInput } from '@gauzy/contracts'; +import { TaskLinkedIssueDTO } from './task-linked-issue.dto'; + +export class UpdateTaskLinkedIssueDTO extends TaskLinkedIssueDTO implements ITaskLinkedIssueUpdateInput { } diff --git a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts index 1c1713443b7..9d2cd13c374 100644 --- a/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts +++ b/packages/core/src/tasks/linked-issue/task-linked-issue.controller.ts @@ -3,19 +3,22 @@ import { Controller, HttpCode, HttpStatus, + Param, Post, + Put, UseGuards, UsePipes, ValidationPipe, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ITaskLinkedIssue, PermissionsEnum } from '@gauzy/contracts'; -import { PermissionGuard, TenantPermissionGuard } from './../../shared/guards'; +import { PermissionGuard, TenantPermissionGuard } from 'shared/guards'; +import { UUIDValidationPipe } from 'shared/pipes'; +import { Permissions } from 'shared/decorators'; +import { CrudController } from 'core/crud'; import { TaskLinkedIssue } from './task-linked-issue.entity'; import { TaskLinkedIssueService } from './task-linked-issue.service'; -import { Permissions } from './../../shared/decorators'; -import { CrudController } from './../../core/crud'; -import { CreateTaskLinkedIssueDTO } from './dto'; +import { CreateTaskLinkedIssueDTO, UpdateTaskLinkedIssueDTO } from './dto'; @ApiTags('Linked Issue') @UseGuards(TenantPermissionGuard, PermissionGuard) @@ -35,8 +38,7 @@ export class TaskLinkedIssueController extends CrudController { * @returns */ @HttpCode(HttpStatus.CREATED) - @UseGuards(PermissionGuard) - @Permissions(PermissionsEnum.ORG_TASK_ADD) + @Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_ADD) @Post() @UsePipes(new ValidationPipe({ whitelist: true })) async create( @@ -44,4 +46,25 @@ export class TaskLinkedIssueController extends CrudController { ): Promise { return await this.taskLinkedIssueService.create(entity); } + + /** + * Update existing Linked Issue + * + * @param id + * @param entity + * @returns + */ + @HttpCode(HttpStatus.ACCEPTED) + @Permissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TASK_EDIT) + @Put(':id') + @UsePipes(new ValidationPipe({ whitelist: true })) + async update( + @Param('id', UUIDValidationPipe) id: ITaskLinkedIssue['id'], + @Body() entity: UpdateTaskLinkedIssueDTO + ): Promise { + return await this.taskLinkedIssueService.create({ + ...entity, + id + }); + } } diff --git a/packages/core/src/tasks/task.entity.ts b/packages/core/src/tasks/task.entity.ts index 85b01e1a2b4..61ea93518ef 100644 --- a/packages/core/src/tasks/task.entity.ts +++ b/packages/core/src/tasks/task.entity.ts @@ -1,13 +1,13 @@ import { - Entity, Column, - ManyToOne, + Entity, + Index, JoinColumn, - RelationId, - OneToMany, - ManyToMany, JoinTable, - Index, + ManyToMany, + ManyToOne, + OneToMany, + RelationId, } from 'typeorm'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { @@ -29,6 +29,9 @@ import { IOrganizationTeam, ITag, ITask, + ITaskPriority, + ITaskSize, + ITaskStatus, ITimeLog, IUser, TaskPriorityEnum, @@ -42,13 +45,16 @@ import { OrganizationProject, OrganizationSprint, OrganizationTeam, + OrganizationTeamEmployee, Tag, + TaskEstimation, + TaskLinkedIssue, + TaskPriority, + TaskSize, + TaskStatus, TenantOrganizationBaseEntity, TimeLog, User, - TaskEstimation, - TaskLinkedIssue, - OrganizationTeamEmployee, } from '../core/entities/internal'; @Entity('task') @@ -143,10 +149,10 @@ export class Task extends TenantOrganizationBaseEntity implements ITask { taskNumber?: string; /* - |-------------------------------------------------------------------------- - | @ManyToOne - |-------------------------------------------------------------------------- - */ + |-------------------------------------------------------------------------- + | @ManyToOne + |-------------------------------------------------------------------------- + */ // Define the parent-child relationship @ApiPropertyOptional({ type: () => Task }) @@ -217,11 +223,71 @@ export class Task extends TenantOrganizationBaseEntity implements ITask { @Column({ nullable: true }) organizationSprintId?: IOrganizationSprint['id']; + /** + * Task Status + */ + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + @IsObject() + @ManyToOne(() => TaskStatus, { + onDelete: 'SET NULL', + }) + @JoinColumn() + taskStatus?: ITaskStatus; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() + @RelationId((it: Task) => it.taskStatus) + @Index() + @Column({ nullable: true, type: 'varchar' }) + taskStatusId?: ITaskStatus['id']; + + /** + * Task Size + */ + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + @IsObject() + @ManyToOne(() => TaskSize, { + onDelete: 'SET NULL', + }) + @JoinColumn() + taskSize?: ITaskSize; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() + @RelationId((it: Task) => it.taskSize) + @Index() + @Column({ nullable: true, type: 'varchar' }) + taskSizeId?: ITaskSize['id']; + + /** + * Task Priority + */ + @ApiPropertyOptional({ type: () => Object }) + @IsOptional() + @IsObject() + @ManyToOne(() => TaskPriority, { + onDelete: 'SET NULL', + }) + @JoinColumn() + taskPriority?: ITaskPriority; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() + @RelationId((it: Task) => it.taskPriority) + @Index() + @Column({ nullable: true, type: 'varchar' }) + taskPriorityId?: ITaskPriority['id']; + /* - |-------------------------------------------------------------------------- - | @OneToMany - |-------------------------------------------------------------------------- - */ + |-------------------------------------------------------------------------- + | @OneToMany + |-------------------------------------------------------------------------- + */ /** * Organization Team Employees @@ -270,10 +336,10 @@ export class Task extends TenantOrganizationBaseEntity implements ITask { linkedIssues?: TaskLinkedIssue[]; /* - |-------------------------------------------------------------------------- - | @ManyToMany - |-------------------------------------------------------------------------- - */ + |-------------------------------------------------------------------------- + | @ManyToMany + |-------------------------------------------------------------------------- + */ /** * Tags diff --git a/packages/core/src/tasks/task.module.ts b/packages/core/src/tasks/task.module.ts index 517851b62f4..752ad952e19 100644 --- a/packages/core/src/tasks/task.module.ts +++ b/packages/core/src/tasks/task.module.ts @@ -21,10 +21,10 @@ import { EmployeeModule } from './../employee/employee.module'; RoleModule, EmployeeModule, OrganizationProjectModule, - CqrsModule, + CqrsModule ], controllers: [TaskController], providers: [TaskService, ...CommandHandlers], exports: [TypeOrmModule, TaskService], }) -export class TaskModule {} +export class TaskModule { } diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index ff45c78c65c..18c9bbdec67 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -180,11 +180,14 @@ export class TaskService extends TenantAwareCrudService { * If additional options found */ query.setFindOptions({ - ...(isNotEmpty(options) && isNotEmpty(options.where) - ? { + ...(isNotEmpty(options) && + isNotEmpty(options.where) && { where: options.where, - } - : {}), + }), + ...(isNotEmpty(options) && + isNotEmpty(options.relations) && { + relations: options.relations, + }), }); query.andWhere( new Brackets((qb: WhereExpressionBuilder) => { diff --git a/packages/core/src/upwork/upwork-authorization.controller.ts b/packages/core/src/upwork/upwork-authorization.controller.ts index b0d90e90fdb..32b5e30f10d 100644 --- a/packages/core/src/upwork/upwork-authorization.controller.ts +++ b/packages/core/src/upwork/upwork-authorization.controller.ts @@ -1,9 +1,12 @@ -import { Controller, Get, Query, Res } from '@nestjs/common'; +import { Controller, Get, HttpException, HttpStatus, Query, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { Response } from 'express'; import { ConfigService } from '@gauzy/config'; -import { Public } from '@gauzy/common'; +import { IUpworkConfig, Public } from '@gauzy/common'; +import { IntegrationEnum } from '@gauzy/contracts'; @ApiTags('Upwork Integrations') +@Public() @Controller() export class UpworkAuthorizationController { @@ -12,27 +15,35 @@ export class UpworkAuthorizationController { ) { } /** - * Upwork Integration Authorization Flow Callback - * - * @param oauth_token - * @param oauth_verifier - * @param res - * @returns - */ - @Public() + * Handle the callback from the Upwork integration. + * + * @param {any} query - The query parameters from the callback. + * @param {Response} response - Express Response object. + */ @Get('callback') - async upworkCallback( - @Query('oauth_token') oauth_token: string, - @Query('oauth_verifier') oauth_verifier: string, - @Res() res: any + async upworkIntegrationCallback( + @Query() query: any, + @Res() response: Response ) { try { - if (oauth_token && oauth_verifier) { - return res.redirect(`${this._config.get('clientBaseUrl')}/#/pages/integrations/upwork?oauth_token=${oauth_token}&oauth_verifier=${oauth_verifier}`); + // Validate the input data (You can use class-validator for validation) + if (!query || !query.oauth_token || !query.oauth_verifier) { + throw new HttpException('Invalid query parameters', HttpStatus.BAD_REQUEST); } - return res.redirect(`${this._config.get('clientBaseUrl')}/#/pages/integrations/upwork`); + + /** Upwork Config Options */ + const upwork = this._config.get('upwork') as IUpworkConfig; + + /** Construct the redirect URL with query parameters */ + const urlParams = new URLSearchParams(); + urlParams.append('oauth_token', query.oauth_token); + urlParams.append('oauth_verifier', query.oauth_verifier); + + /** Redirect to the URL */ + return response.redirect(`${upwork.postInstallUrl}?${urlParams.toString()}`); } catch (error) { - return res.redirect(`${this._config.get('clientBaseUrl')}/#/pages/integrations/upwork`); + // Handle errors and return an appropriate error response + throw new HttpException(`Failed to add ${IntegrationEnum.UPWORK} integration: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); } } } diff --git a/packages/core/src/upwork/upwork.service.ts b/packages/core/src/upwork/upwork.service.ts index 9b6c3ee381a..737a7cf4dbf 100644 --- a/packages/core/src/upwork/upwork.service.ts +++ b/packages/core/src/upwork/upwork.service.ts @@ -34,7 +34,7 @@ import { ITimeLog } from '@gauzy/contracts'; import { - IntegrationTenantCreateCommand, + IntegrationTenantFirstOrCreateCommand, IntegrationTenantGetCommand } from '../integration-tenant/commands'; import { @@ -165,7 +165,7 @@ export class UpworkService { this._upworkApi = new UpworkApi(config); - const authUrl = environment.upworkConfig.callbackUrl; + const authUrl = environment.upwork.callbackUrl; console.log(`Upwork callback URL: ${authUrl}`); @@ -179,7 +179,14 @@ export class UpworkService { } await this.commandBus.execute( - new IntegrationTenantCreateCommand({ + new IntegrationTenantFirstOrCreateCommand({ + name: IntegrationEnum.UPWORK, + integration: { + provider: IntegrationEnum.UPWORK + }, + tenantId, + organizationId, + }, { tenantId, organizationId, name: IntegrationEnum.UPWORK, @@ -201,7 +208,11 @@ export class UpworkService { settingsName: 'requestTokenSecret', settingsValue: requestTokenSecret } - ] + ].map((setting) => ({ + ...setting, + tenantId, + organizationId, + })) }) ); return resolve({ diff --git a/packages/desktop-libs/src/lib/desktop-ipc.ts b/packages/desktop-libs/src/lib/desktop-ipc.ts index 0e960153e25..3ca2f62d7ef 100644 --- a/packages/desktop-libs/src/lib/desktop-ipc.ts +++ b/packages/desktop-libs/src/lib/desktop-ipc.ts @@ -73,8 +73,8 @@ export function ipcMainHandler( API_BASE_URL: arg.serverUrl ? arg.serverUrl : arg.port - ? `http://localhost:${arg.port}` - : `http://localhost:${config.API_DEFAULT_PORT}`, + ? `http://localhost:${arg.port}` + : `http://localhost:${config.API_DEFAULT_PORT}`, IS_INTEGRATED_DESKTOP: arg.isLocalServer, }; return await startServer(arg); @@ -185,16 +185,20 @@ export function ipcMainHandler( } else { await logout(); } + try { timeTrackerWindow.webContents.send( 'preferred_language_change', TranslateService.preferredLanguage ); + const lastTime = await TimerData.getLastCaptureTimeSlot( knex, LocalStore.beforeRequestParams() ); + console.log('Last Capture Time (Desktop IPC):', lastTime); + await offlineMode.connectivity(); console.log( 'Network state', @@ -207,6 +211,7 @@ export function ipcMainHandler( }); await countIntervalQueue(timeTrackerWindow, false); await sequentialSyncQueue(timeTrackerWindow); + await latestScreenshots(timeTrackerWindow); } catch (error) { throw new UIError('500', error, 'IPCINIT'); @@ -418,15 +423,21 @@ export function ipcTimer( ipcMain.handle('START_TIMER', async (event, arg) => { try { powerManager = new DesktopPowerManager(timeTrackerWindow); + powerManagerPreventSleep = new PowerManagerPreventDisplaySleep( powerManager ); + powerManagerDetectInactivity = new PowerManagerDetectInactivity( powerManager ); + new DesktopOsInactivityHandler(powerManagerDetectInactivity); + const setting = LocalStore.getStore('appSetting'); + log.info(`Timer Start: ${moment().format()}`); + store.set({ project: { projectId: arg.projectId, @@ -436,8 +447,10 @@ export function ipcTimer( organizationContactId: arg.organizationContactId, }, }); - // Check api connection before to start + + // Check API connection before starting await offlineMode.connectivity(); + // Start Timer const timerResponse = await timerHandler.startTimer( setupWindow, @@ -448,6 +461,7 @@ export function ipcTimer( settingWindow.webContents.send('app_setting_update', { setting: LocalStore.getStore('appSetting'), }); + if (setting && setting.preventDisplaySleep) { powerManagerPreventSleep.start(); } diff --git a/packages/desktop-ui-lib/src/lib/services/index.ts b/packages/desktop-ui-lib/src/lib/services/index.ts index e08837c85f1..197b3c63a4d 100644 --- a/packages/desktop-ui-lib/src/lib/services/index.ts +++ b/packages/desktop-ui-lib/src/lib/services/index.ts @@ -21,3 +21,4 @@ export * from './image-cache.service'; export * from './language-cache.service'; export * from './time-tracker-date.manager'; export * from './time-zone-manager'; +export * from './task-status-cache.service'; diff --git a/packages/desktop-ui-lib/src/lib/services/store.service.ts b/packages/desktop-ui-lib/src/lib/services/store.service.ts index 7e2a10b0947..d827565974c 100644 --- a/packages/desktop-ui-lib/src/lib/services/store.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/store.service.ts @@ -11,16 +11,17 @@ import { IFeatureToggle, IFeatureOrganization, FeatureEnum, - ComponentLayoutStyleEnum + ComponentLayoutStyleEnum, + ITaskStatus, } from '@gauzy/contracts'; import { Injectable } from '@angular/core'; import { StoreConfig, Store as AkitaStore, Query } from '@datorama/akita'; import { ComponentEnum, - SYSTEM_DEFAULT_LAYOUT + SYSTEM_DEFAULT_LAYOUT, } from '../constants/layout.constants'; import { map } from 'rxjs/operators'; -import { merge, Subject } from 'rxjs'; +import { merge, Observable, Subject } from 'rxjs'; import * as _ from 'underscore'; export interface AppState { @@ -35,6 +36,7 @@ export interface AppState { featureOrganizations: IFeatureOrganization[]; featureTenant: IFeatureOrganization[]; isOffline: boolean; + statuses: ITaskStatus[]; } export interface PersistState { @@ -58,6 +60,7 @@ export function createInitialAppState(): AppState { featureOrganizations: [], featureTenant: [], isOffline: false, + statuses: [], } as AppState; } @@ -80,7 +83,7 @@ export function createInitialPersistState(): PersistState { preferredLanguage, componentLayout, tenantId, - host + host, } as PersistState; } @@ -121,7 +124,7 @@ export class Store { protected appQuery: AppQuery, protected persistStore: PersistStore, protected persistQuery: PersistQuery - ) { } + ) {} user$ = this.appQuery.select((state) => state.user); selectedOrganization$ = this.appQuery.select( @@ -133,9 +136,15 @@ export class Store { (state) => state.userRolePermissions ); featureToggles$ = this.appQuery.select((state) => state.featureToggles); + featureOrganizations$ = this.appQuery.select( (state) => state.featureOrganizations ); + + statuses$: Observable = this.appQuery.select( + (state) => state.statuses + ); + featureTenant$ = this.appQuery.select((state) => state.featureTenant); preferredLanguage$ = this.persistQuery.select( (state) => state.preferredLanguage @@ -315,6 +324,17 @@ export class Store { }); } + get statuses(): ITaskStatus[] { + const { statuses } = this.appQuery.getValue(); + return statuses; + } + + set statuses(statuses: ITaskStatus[]) { + this.appStore.update({ + statuses, + }); + } + get featureOrganizations(): IFeatureOrganization[] { const { featureOrganizations } = this.appQuery.getValue(); return featureOrganizations; @@ -385,8 +405,8 @@ export class Store { getDateFromOrganizationSettings() { const dateObj = this.selectedDate; switch ( - this.selectedOrganization && - this.selectedOrganization.defaultValueDateType + this.selectedOrganization && + this.selectedOrganization.defaultValueDateType ) { case DefaultValueDateTypeEnum.TODAY: { return new Date(Date.now()); @@ -483,7 +503,7 @@ export class Store { set tenantId(value: string) { this.persistStore.update({ - tenantId: value + tenantId: value, }); } @@ -494,7 +514,7 @@ export class Store { set host(value: string) { this.persistStore.update({ - host: value - }) + host: value, + }); } } diff --git a/packages/desktop-ui-lib/src/lib/services/task-status-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/task-status-cache.service.ts new file mode 100644 index 00000000000..6b934d2203a --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/services/task-status-cache.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { ITaskStatus } from '@gauzy/contracts'; +import { AbstractCacheService } from './abstract-cache.service'; +import { StorageService } from './storage.service'; +import { Store } from './store.service'; + +@Injectable({ + providedIn: 'root', +}) +export class TaskStatusCacheService extends AbstractCacheService< + ITaskStatus[] +> { + constructor( + protected _storageService: StorageService, + protected _store: Store + ) { + super(_storageService, _store); + this.prefix = TaskStatusCacheService.name.toString(); + this.duration = 24 * 3600 * 1000 * 7; // 1 week + } +} diff --git a/packages/desktop-ui-lib/src/lib/settings/settings.component.ts b/packages/desktop-ui-lib/src/lib/settings/settings.component.ts index 216ae405362..bb9bda33aee 100644 --- a/packages/desktop-ui-lib/src/lib/settings/settings.component.ts +++ b/packages/desktop-ui-lib/src/lib/settings/settings.component.ts @@ -187,18 +187,18 @@ export class SettingsComponent implements OnInit, AfterViewInit { title: 'Github', fields: [ { - name: 'GITHUB_CLIENT_ID', - field: 'GITHUB_CLIENT_ID', + name: 'GAUZY_GITHUB_CLIENT_ID', + field: 'GAUZY_GITHUB_CLIENT_ID', value: '' }, { - name: 'GITHUB_CLIENT_SECRET', - field: 'GITHUB_CLIENT_SECRET', + name: 'GAUZY_GITHUB_CLIENT_SECRET', + field: 'GAUZY_GITHUB_CLIENT_SECRET', value: '' }, { - name: 'GITHUB_CALLBACK_URL', - field: 'GITHUB_CALLBACK_URL', + name: 'GAUZY_GITHUB_CALLBACK_URL', + field: 'GAUZY_GITHUB_CALLBACK_URL', value: '' } ] diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts index 959dc64c7a8..db66e12197b 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts @@ -9,9 +9,9 @@ import { TaskStatusEnum, } from '@gauzy/contracts'; import { NbToastrService } from '@nebular/theme'; -import { Color, rgbString } from '@kurkle/color'; import * as moment from 'moment'; import { TranslateService } from '@ngx-translate/core'; +import { ColorAdapter } from '../utils'; @Component({ selector: 'ngx-tasks', @@ -77,9 +77,7 @@ export class TasksComponent implements OnInit { })(); this.form = new FormGroup({ description: new FormControl(null), - dueDate: new FormControl( - moment().add(1, 'day').utc().toDate() - ), + dueDate: new FormControl(moment().add(1, 'day').utc().toDate()), estimate: new FormControl(null), estimateDays: new FormControl(null, [Validators.min(0)]), estimateHours: new FormControl(null, [ @@ -106,7 +104,7 @@ export class TasksComponent implements OnInit { try { this.projects = await this.timeTrackerService.getProjects({ organizationContactId: null, - ...user + ...user, }); } catch (error) { console.error( @@ -161,7 +159,7 @@ export class TasksComponent implements OnInit { this.isAddTask.emit(false); this.newTaskCallback.emit({ isSuccess: true, - message: this.translate.instant('TOASTR.MESSAGE.CREATED') + message: this.translate.instant('TOASTR.MESSAGE.CREATED'), }); } catch (error) { console.log(error); @@ -207,43 +205,11 @@ export class TasksComponent implements OnInit { }; public background(bgColor: string) { - const color = new Color(bgColor); - return color.valid ? bgColor : this._test(bgColor); + return ColorAdapter.background(bgColor); } - public backgroundContrast(bgColor) { - let color = new Color(bgColor); - color = color.valid ? color : new Color(this._hex2rgb(bgColor)); - const MIN_THRESHOLD = 128; - const MAX_THRESHOLD = 186; - const contrast = color.rgb - ? color.rgb.r * 0.299 + color.rgb.g * 0.587 + color.rgb.b * 0.114 - : null; - if (contrast < MIN_THRESHOLD) { - return '#ffffff'; - } else if (contrast > MAX_THRESHOLD) { - return '#000000'; - } - } - - private _hex2rgb(hex: string) { - hex = this._test(hex); - return rgbString({ - r: parseInt(hex.slice(1, 3), 16), - g: parseInt(hex.slice(3, 5), 16), - b: parseInt(hex.slice(5, 7), 16), - a: 1, - }); - } - - private _test(hex: string): string { - const regex = /^#[0-9A-F]{6}$/i; - if (regex.test(hex)) { - return hex; - } else { - hex = '#' + hex; - return regex.test(hex) ? hex : '#000000'; - } + public backgroundContrast(bgColor: string) { + return ColorAdapter.contrast(bgColor); } private _formatStatus(name: string): string { diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/pipes/replace.pipe.ts b/packages/desktop-ui-lib/src/lib/time-tracker/pipes/replace.pipe.ts new file mode 100644 index 00000000000..b81c3866999 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/pipes/replace.pipe.ts @@ -0,0 +1,26 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { isRegExp, isString, isUndefined } from 'underscore'; + +@Pipe({ + name: 'replace', +}) +export class ReplacePipe implements PipeTransform { + transform(input: any, pattern: any, replacement: any): any { + if ( + !isString(input) || + isUndefined(pattern) || + isUndefined(replacement) + ) { + return input; + } + if (isRegExp(pattern)) { + return input.replace(pattern, replacement); + } else { + return this.replaceAll(input, pattern, replacement); + } + } + + replaceAll(string, search, replace) { + return string.split(search).join(replace); + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.html new file mode 100644 index 00000000000..df13c27b7b1 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.html @@ -0,0 +1,6 @@ + diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.scss new file mode 100644 index 00000000000..7747a396967 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.scss @@ -0,0 +1,16 @@ +.badge { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + position: relative; + width: fit-content; + height: 1.5rem; + font-size: 12px; + font-weight: 600; + letter-spacing: 0; + text-align: left; + padding: 2px 4px; + line-height: 1; + border-radius: calc(var(--border-radius) / 3); +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.ts new file mode 100644 index 00000000000..046be699ad1 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-default/task-badge-default.component.ts @@ -0,0 +1,39 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { TaskStatusEnum } from '@gauzy/contracts'; +import { NbComponentStatus } from '@nebular/theme'; + +@Component({ + selector: 'gauzy-task-badge-default', + templateUrl: './task-badge-default.component.html', + styleUrls: ['./task-badge-default.component.scss'], +}) +export class TaskBadgeDefaultComponent implements OnInit { + @Input() taskBadge: string; + public status: NbComponentStatus; + + ngOnInit(): void { + switch (this.taskBadge) { + case TaskStatusEnum.OPEN: + this.status = 'basic'; + break; + case TaskStatusEnum.IN_PROGRESS: + this.status = 'info'; + break; + case TaskStatusEnum.READY_FOR_REVIEW: + this.status = 'warning'; + break; + case TaskStatusEnum.IN_REVIEW: + this.status = 'info'; + break; + case TaskStatusEnum.COMPLETED: + this.status = 'success'; + break; + case TaskStatusEnum.BLOCKED: + this.status = 'danger'; + break; + default: + this.status = 'basic'; + break; + } + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.html new file mode 100644 index 00000000000..b84283760f3 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.html @@ -0,0 +1,15 @@ +
+
+
+
+ badge +
+
{{ name | replace : '-' : ' ' | titlecase }}
+
+
+
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.scss new file mode 100644 index 00000000000..a76cfac5321 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.scss @@ -0,0 +1,18 @@ +.badge-color { + display: flex; + padding: 4px; + width: fit-content; + line-height: 1; + border-radius: var(--border-radius); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + gap: 4px; + align-items: center; + font-weight: 600; + + .badge-img { + width: 18px; + height: 100%; + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.ts new file mode 100644 index 00000000000..099d034cc41 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-badge-view/task-badge-view.component.ts @@ -0,0 +1,47 @@ +import { Component, Input } from '@angular/core'; +import { ITaskPriority, ITaskSize, ITaskStatus } from '@gauzy/contracts'; +import { ColorAdapter } from '../../../utils'; + +export type ITaskBadge = ITaskStatus | ITaskSize | ITaskPriority; + +@Component({ + selector: 'gauzy-task-badge-view', + templateUrl: './task-badge-view.component.html', + styleUrls: ['./task-badge-view.component.scss'], +}) +export class TaskBadgeViewComponent { + constructor() { + this._taskBadge = null; + } + + private _taskBadge: ITaskBadge; + + public get taskBadge(): ITaskBadge { + return this._taskBadge; + } + + @Input() + public set taskBadge(value: ITaskBadge) { + this._taskBadge = value; + } + + public get textColor() { + return ColorAdapter.contrast(this.taskBadge.color); + } + + public get backgroundColor() { + return ColorAdapter.background(this.taskBadge.color); + } + + public get icon() { + return this.taskBadge.fullIconUrl; + } + + public get name() { + return this.taskBadge.name; + } + + public get imageFilter() { + return ColorAdapter.hexToHsl(this.taskBadge.color); + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.html index 2d0498dee5d..454a5e1bffb 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.html @@ -1,11 +1,24 @@
-
{{ isEdit ? 'Edit:' : 'Estimate:' }}
+
+ {{ + ((isEditDisabled$ | async) + ? 'TASKS_PAGE.COMPLETED' + : isEdit + ? 'TIMESHEET.EDIT' + : 'TASKS_PAGE.ESTIMATE' + ) | translate + }}: +
{{ (estimate$ | async) || 0 | durationFormat : 'HH:mm' }}
- +
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.ts index 41a4ebbb005..5382f2737d3 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-estimate/task-estimate.component.ts @@ -3,6 +3,7 @@ import { ITaskRender } from '../task-render.component'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { TaskStatusEnum } from '@gauzy/contracts'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -27,6 +28,15 @@ export class TaskEstimateComponent { ); } + public get isEditDisabled$(): Observable { + return this.task$.pipe( + map( + (task: ITaskRender) => task?.status === TaskStatusEnum.COMPLETED + ), + untilDestroyed(this) + ); + } + public update(event: number): void { this.isEdit = false; if (isNaN(Number(event))) return; diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html index 8b1df88359a..07758fdca9e 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html @@ -11,11 +11,16 @@
{{ title }}
-
{{ size }}
-
{{ status }}
-
{{ priority }}
+
+ +
+
+ +
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.ts index 6529d14f88a..96e330b55f7 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.ts @@ -3,6 +3,7 @@ import { TaskRenderComponent } from '../task-render.component'; import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { ITaskPriority, ITaskSize } from '@gauzy/contracts'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -16,19 +17,15 @@ export class TaskRenderCellComponent extends TaskRenderComponent { } public get number(): string { - return `#${this.task.taskNumber}`; + return `#${this.task.taskNumber || this.buildTaskNumber()}`; } - public get status(): string { - return this.task.status; + public get size(): ITaskSize { + return this.task.taskSize; } - public get size(): string { - return this.task.size; - } - - public get priority(): string { - return this.task.priority; + public get priority(): ITaskPriority { + return this.task.taskPriority; } public get isSelected$(): Observable { @@ -37,4 +34,12 @@ export class TaskRenderCellComponent extends TaskRenderComponent { untilDestroyed(this) ); } + + private buildTaskNumber() { + if (!this.task.prefix || !this.task.number) return; + return this.task.prefix + .concat('-') + .concat(String(this.task.number)) + .toUpperCase(); + } } diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render.module.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render.module.ts index a25a9d0ae7b..345566433d3 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render.module.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render.module.ts @@ -1,8 +1,10 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { + NbBadgeModule, NbButtonModule, NbIconModule, + NbPopoverModule, NbProgressBarModule, NbTooltipModule, } from '@nebular/theme'; @@ -16,6 +18,10 @@ import { TaskEstimateInputComponent } from './task-estimate/task-estimate-input/ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TaskDueDateComponent } from './task-due-date/task-due-date.component'; import { TranslateModule } from '@ngx-translate/core'; +import { TaskBadgeViewComponent } from './task-badge-view/task-badge-view.component'; +import { TaskStatusComponent } from './task-status/task-status.component'; +import { TaskBadgeDefaultComponent } from './task-badge-default/task-badge-default.component'; +import { ReplacePipe } from '../pipes/replace.pipe'; @NgModule({ declarations: [ @@ -26,6 +32,10 @@ import { TranslateModule } from '@ngx-translate/core'; TaskRenderCellComponent, TaskEstimateInputComponent, TaskDueDateComponent, + TaskBadgeViewComponent, + ReplacePipe, + TaskStatusComponent, + TaskBadgeDefaultComponent, ], imports: [ CommonModule, @@ -37,6 +47,8 @@ import { TranslateModule } from '@ngx-translate/core'; DesktopDirectiveModule, NbButtonModule, TranslateModule, + NbPopoverModule, + NbBadgeModule, ], }) export class TaskRenderModule {} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.html new file mode 100644 index 00000000000..d3c677c3c19 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.html @@ -0,0 +1,27 @@ + + + + + +
+ + + +
+
+ + + + diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.scss new file mode 100644 index 00000000000..5636fa78186 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.scss @@ -0,0 +1,14 @@ +.view { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px; +} + +.status-view { + cursor: pointer; +} + +::ng-deep nb-popover > span.arrow { + display: none; +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.ts new file mode 100644 index 00000000000..697d9c2b573 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-status/task-status.component.ts @@ -0,0 +1,50 @@ +import { Component, EventEmitter, OnInit } from '@angular/core'; +import { ITaskRender, TaskRenderComponent } from '../task-render.component'; +import { ITaskStatus, TaskStatusEnum } from '@gauzy/contracts'; +import { Store } from '../../../services'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + selector: 'gauzy-task-status', + templateUrl: './task-status.component.html', + styleUrls: ['./task-status.component.scss'], +}) +export class TaskStatusComponent extends TaskRenderComponent implements OnInit { + public statuses$: Observable; + public updated: EventEmitter; + + constructor(private readonly store: Store) { + super(); + this.updated = new EventEmitter(); + } + + public get taskStatus$(): Observable { + return this.task$.pipe( + map((task: ITaskRender) => task?.taskStatus), + untilDestroyed(this) + ); + } + + public get status$(): Observable { + return this.task$.pipe( + map((task: ITaskRender) => String(task?.status)), + untilDestroyed(this) + ); + } + + public updateStatus(taskStatus: ITaskStatus) { + this.rowData = { + ...this.task, + status: taskStatus.name as TaskStatusEnum, + taskStatus, + }; + this.updated.emit(taskStatus); + } + + public ngOnInit(): void { + this.statuses$ = this.store.statuses$; + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html index 1ff642f2884..cbd1e92dadc 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html @@ -1,11 +1,11 @@ @@ -22,10 +22,10 @@
@@ -55,15 +55,15 @@
@@ -104,42 +104,42 @@ >
@@ -533,15 +555,17 @@
-
- +
+
@@ -572,28 +594,28 @@
@@ -629,25 +651,25 @@
- +
- - + + {{ 'TIMER_TRACKER.DIALOG.WARNING' | translate }} @@ -741,15 +763,15 @@
-
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts index fcabea734d8..7d9da5183e8 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts @@ -44,6 +44,7 @@ import { IOrganizationContact, ITask, ITasksStatistics, + ITaskStatus, ITaskUpdateInput, LanguagesEnum, PermissionsEnum, @@ -79,6 +80,7 @@ import { } from '../always-on/always-on.service'; import { TaskDurationComponent, TaskProgressComponent } from './task-render'; import { TaskRenderCellComponent } from './task-render/task-render-cell/task-render-cell.component'; +import { TaskStatusComponent } from './task-render/task-status/task-status.component'; enum TimerStartMode { MANUAL = 'manual', @@ -432,11 +434,20 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { statistics: ITasksStatistics[] ): (ITask & ITasksStatistics)[] { let arr: (ITask & ITasksStatistics)[] = []; - arr = arr.concat(tasks, statistics); + arr = arr.concat(statistics, tasks); return arr.reduce((result, current) => { const existing = result.find((item: any) => item.id === current.id); if (existing) { - Object.assign(existing, current); + const updatedAtMoment = moment(existing?.updatedAt).utc(true); + Object.assign( + existing, + current, + updatedAtMoment.isAfter(current?.updatedAt) + ? { + updatedAt: updatedAtMoment.toISOString(), + } + : {} + ); } else { result.push(current); } @@ -752,6 +763,45 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { }); }, }, + taskStatus: { + title: this._translateService.instant('SM_TABLE.STATUS'), + type: 'custom', + renderComponent: TaskStatusComponent, + onComponentInitFunction: ( + instance: TaskStatusComponent + ) => { + instance.updated.subscribe({ + next: async (taskStatus: ITaskStatus) => { + const { tenantId, organizationId } = + this._store; + const id = instance.task.id; + const title = instance.task.title; + const status = + taskStatus.name as TaskStatusEnum; + const taskUpdateInput: ITaskUpdateInput = { + organizationId, + tenantId, + status, + title, + taskStatus, + }; + await this.timeTrackerService.updateTask( + id, + taskUpdateInput + ); + this.toastrService.success( + this._translateService.instant( + 'TOASTR.MESSAGE.UPDATED' + ) + ); + this.refreshTimer(); + }, + error: (err: any) => { + console.warn(err); + }, + }); + }, + }, }, hideSubHeader: true, actions: false, @@ -766,6 +816,22 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { }; } + private async loadStatuses(): Promise { + if (!this._store.organizationId && !this._store.tenantId) { + return; + } + const { organizationId, tenantId } = this._store; + this._store.statuses = await this.timeTrackerService.statuses({ + tenantId, + organizationId, + ...(this.projectSelect + ? { + projectId: this.projectSelect, + } + : {}), + }); + } + ngOnInit(): void { this._sourceData$ = new BehaviorSubject( new LocalDataSource(this.tableData) @@ -955,6 +1021,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this.electronService.ipcRenderer.send('show_ao'); } const parallelizedTasks: Promise[] = [ + this.loadStatuses(), this.getClient(arg), this.getProjects(arg), this.getTask(arg), @@ -1609,6 +1676,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { console.log('stop tracking'); await this.stopTimer(onClick); } + this.refreshTimer(); } else { this.loading = false; console.log('Error', 'validation failed'); @@ -1672,6 +1740,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { }); } await this._toggle(timer, onClick); + await this.updateOrganizationTeamEmployee(); this.electronService.ipcRenderer.send('request_permission'); } catch (error) { this._startMode = TimerStartMode.STOP; @@ -2685,4 +2754,35 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this._isLockSyncProcess = false; } } + + public async updateOrganizationTeamEmployee(): Promise { + try { + if (!this.taskSelect) { + return; + } + const [task] = this._tasks$ + .getValue() + .filter(({ id }) => id === this.taskSelect); + // TODO: Add task teams selector + const organizationTeamId = task?.teams[0]?.id || null; + if (!organizationTeamId) { + return; + } + const { tenantId, organizationId } = this._store; + const { employeeId } = this._store.user; + const activeTaskId = this.taskSelect; + const payload = { + activeTaskId, + tenantId, + organizationId, + organizationTeamId, + }; + await this.timeTrackerService.updateOrganizationTeamEmployee( + employeeId, + payload + ); + } catch (error) { + console.error(error); + } + } } diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts index b1c37849b9c..fe31530aeb1 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts @@ -6,14 +6,18 @@ import { catchError, map, shareReplay, tap } from 'rxjs/operators'; import { firstValueFrom, throwError } from 'rxjs'; import { toParams } from '@gauzy/common-angular'; import { - TimeLogSourceEnum, - TimeLogType, - IOrganizationProjectCreateInput, - IOrganizationProject, - IOrganizationContactCreateInput, - IOrganizationContact, IGetTasksStatistics, + IOrganizationContact, + IOrganizationContactCreateInput, + IOrganizationProject, + IOrganizationProjectCreateInput, + IOrganizationTeamEmployee, + IPagination, + ITaskStatus, + ITaskStatusFindInput, ITaskUpdateInput, + TimeLogSourceEnum, + TimeLogType, } from '@gauzy/contracts'; import { ClientCacheService } from '../services/client-cache.service'; import { TaskCacheService } from '../services/task-cache.service'; @@ -25,12 +29,22 @@ import { TagCacheService } from '../services/tag-cache.service'; import { TimeLogCacheService } from '../services/time-log-cache.service'; import { LoggerService } from '../electron/services'; import { API_PREFIX } from '../constants/app.constants'; -import { Store, TimeTrackerDateManager } from '../services'; +import { + Store, + TaskStatusCacheService, + TimeTrackerDateManager, +} from '../services'; @Injectable({ providedIn: 'root', }) export class TimeTrackerService { + AW_HOST = 'http://localhost:5600'; + token = ''; + userId = ''; + employeeId = ''; + buckets: any = {}; + constructor( private readonly http: HttpClient, private readonly _clientCacheService: ClientCacheService, @@ -42,15 +56,10 @@ export class TimeTrackerService { private readonly _userOrganizationService: UserOrganizationService, private readonly _timeLogService: TimeLogCacheService, private readonly _loggerService: LoggerService, - private readonly _store: Store + private readonly _store: Store, + private readonly _taskStatusCacheService: TaskStatusCacheService ) {} - AW_HOST = 'http://localhost:5600'; - token = ''; - userId = ''; - employeeId = ''; - buckets: any = {}; - createAuthorizationHeader(headers: Headers) { headers.append('Authorization', 'Basic ' + btoa('username:password')); } @@ -66,14 +75,32 @@ export class TimeTrackerService { } : {}), }, + relations: [ + 'project', + 'tags', + 'teams', + 'teams.members', + 'teams.members.employee', + 'teams.members.employee.user', + 'creator', + 'organizationSprint', + 'taskStatus', + 'taskSize', + 'taskPriority', + ], + join: { + alias: 'task', + leftJoinAndSelect: { + members: 'task.members', + user: 'members.user', + }, + }, }; let tasks$ = this._taskCacheService.getValue(request); if (!tasks$) { tasks$ = this.http .get(`${API_PREFIX}/tasks/employee/${values.employeeId}`, { - params: toParams({ - ...request, - }), + params: toParams(request), }) .pipe( map((response: any) => response), @@ -649,6 +676,7 @@ export class TimeTrackerService { ) .pipe( tap(() => this._taskCacheService.clear()), + tap(() => this._taskStatusCacheService.clear()), catchError((error) => { error.error = { ...error.error, @@ -658,4 +686,40 @@ export class TimeTrackerService { ) ); } + + public async statuses( + params: ITaskStatusFindInput + ): Promise { + let taskStatuses$ = this._taskStatusCacheService.getValue(params); + if (!taskStatuses$) { + taskStatuses$ = this.http + .get>(`${API_PREFIX}/task-statuses`, { + params: toParams({ ...params }), + }) + .pipe( + map((res) => res.items), + shareReplay(1) + ); + this._taskStatusCacheService.setValue(taskStatuses$, params); + } + return firstValueFrom(taskStatuses$); + } + + public async updateOrganizationTeamEmployee( + employeeId: string, + values: Partial + ): Promise { + const params = { + organizationId: values.organizationId, + activeTaskId: values.activeTaskId, + organizationTeamId: values.organizationTeamId, + tenantId: values.tenantId, + }; + return firstValueFrom( + this.http.put( + `${API_PREFIX}/organization-team-employee/${employeeId}`, + params + ) + ); + } } diff --git a/packages/desktop-ui-lib/src/lib/utils/color-adapter.ts b/packages/desktop-ui-lib/src/lib/utils/color-adapter.ts new file mode 100644 index 00000000000..6abcfa9ffae --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/utils/color-adapter.ts @@ -0,0 +1,49 @@ +import { Color, rgbString } from '@kurkle/color'; + +export class ColorAdapter { + public static hex2Rgb(hex: string) { + hex = this.normalize(hex); + return rgbString({ + r: parseInt(hex.slice(1, 3), 16), + g: parseInt(hex.slice(3, 5), 16), + b: parseInt(hex.slice(5, 7), 16), + a: 1, + }); + } + + public static normalize(hex: string): string { + const regex = /^#[0-9A-F]{6}$/i; + if (regex.test(hex)) { + return hex; + } else { + hex = '#' + hex; + return regex.test(hex) ? hex : '#000000'; + } + } + + public static contrast(bgColor: string) { + let color = new Color(bgColor); + color = color.valid ? color : new Color(this.hex2Rgb(bgColor)); + const MIN_THRESHOLD = 128; + const MAX_THRESHOLD = 186; + const contrast = color.rgb + ? color.rgb.r * 0.299 + color.rgb.g * 0.587 + color.rgb.b * 0.114 + : null; + if (contrast < MIN_THRESHOLD) { + return '#ffffff'; + } else if (contrast > MAX_THRESHOLD) { + return '#000000'; + } + } + + public static background(bgColor: string) { + const color = new Color(bgColor); + return color.valid ? bgColor : this.normalize(bgColor); + } + + public static hexToHsl(hexColor: string): string { + let color = new Color(hexColor); + color = color.valid ? color : new Color(this.hex2Rgb(hexColor)); + return color.hslString(); + } +} diff --git a/packages/desktop-ui-lib/src/lib/utils/index.ts b/packages/desktop-ui-lib/src/lib/utils/index.ts new file mode 100644 index 00000000000..935f5bfda47 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './color-adapter'; diff --git a/packages/plugins/integration-github/README.md b/packages/plugins/integration-github/README.md index 9a6cc7d3360..5988c8f5a56 100644 --- a/packages/plugins/integration-github/README.md +++ b/packages/plugins/integration-github/README.md @@ -5,3 +5,7 @@ This library was generated with [Nx](https://nx.dev). ## Running unit tests Run `ng test integration-github` to execute the unit tests via [Jest](https://jestjs.io). + +## Credits + +In this plugin, we used code from https://github.com/yieldbits/nestjs (MIT license, https://github.com/yieldbits/nestjs/blob/main/LICENSE). diff --git a/packages/plugins/integration-github/package.json b/packages/plugins/integration-github/package.json index 0abd24f6142..78c183a3c68 100644 --- a/packages/plugins/integration-github/package.json +++ b/packages/plugins/integration-github/package.json @@ -28,7 +28,16 @@ }, "keywords": [], "dependencies": { - + "@nestjs/common": "^9.2.1", + "@nestjs/core": "^9.2.1", + "@octokit/rest": "^18.0.0", + "chalk": "4.1.2", + "express": "^4.17.2", + "octokit": "2.1.0", + "pino-std-serializers": "^6.2.2", + "probot": "^12.3.1", + "smee-client": "^1.2.3", + "underscore": "^1.13.3" }, "devDependencies": { "@types/node": "^17.0.33", diff --git a/packages/plugins/integration-github/src/hook-metadata.accessor.ts b/packages/plugins/integration-github/src/hook-metadata.accessor.ts new file mode 100644 index 00000000000..970e8538624 --- /dev/null +++ b/packages/plugins/integration-github/src/hook-metadata.accessor.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { EmitterWebhookEventName } from '@octokit/webhooks/dist-types/types'; + +@Injectable() +export class HookMetadataAccessor { + constructor(private readonly reflector: Reflector) { } + + /** + * Get the webhook events associated with a target. + * @param target A function or constructor representing the target class or controller. + * @returns An array of EmitterWebhookEventName that represent the webhook events. + */ + getWebhookEvents(target: () => any): EmitterWebhookEventName[] { + // Retrieve the metadata for HOOK_EVENTS, if available, from the target. + // HOOK_EVENTS metadata should contain eventOrEvents property. + return this.reflector.get('HOOK_EVENTS', target)?.eventOrEvents; + } +} diff --git a/packages/plugins/integration-github/src/hook.controller.ts b/packages/plugins/integration-github/src/hook.controller.ts new file mode 100644 index 00000000000..9654fcd4ba7 --- /dev/null +++ b/packages/plugins/integration-github/src/hook.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Post, Req, Type } from '@nestjs/common'; +import { Public } from '@gauzy/common'; +import { Request } from 'express'; +import { ProbotDiscovery } from './probot.discovery'; + +/** + * Factory function to create a NestJS controller class for handling webhook hooks. + * @param path The path at which the controller should listen for webhook requests. + */ +export function getControllerClass({ path }): Type { + @Public() + @Controller() + class HookController { + constructor(private readonly probotDiscovery: ProbotDiscovery) { } + + /** + * Endpoint for receiving webhook requests. + * @param req The Express request object. + */ + @Post([path]) + async hooks(@Req() req: Request) { + // Forward the request to ProbotDiscovery for processing. + return await this.probotDiscovery.receiveHook(req); + } + } + + return HookController; +} diff --git a/packages/plugins/integration-github/src/hook.decorator.ts b/packages/plugins/integration-github/src/hook.decorator.ts new file mode 100644 index 00000000000..b104ae3fe20 --- /dev/null +++ b/packages/plugins/integration-github/src/hook.decorator.ts @@ -0,0 +1,12 @@ +import { applyDecorators, SetMetadata } from '@nestjs/common'; +import { EmitterWebhookEventName } from '@octokit/webhooks/dist-types/types'; + +/** + * Sets up hook trigger on functions. + * @param eventOrEvents The GitHub webhook event(s) to trigger this function. + */ +export function Hook( + eventOrEvents: EmitterWebhookEventName | EmitterWebhookEventName[] +): MethodDecorator { + return applyDecorators(SetMetadata('HOOK_EVENTS', { eventOrEvents })); +} diff --git a/packages/plugins/integration-github/src/index.ts b/packages/plugins/integration-github/src/index.ts index e69de29bb2d..65c4483a49e 100644 --- a/packages/plugins/integration-github/src/index.ts +++ b/packages/plugins/integration-github/src/index.ts @@ -0,0 +1,7 @@ +export * from './probot.types'; +export * from './probot.module'; +export * from './hook.decorator'; +export * from './probot.helpers'; +export * from './probot.discovery'; + +export * from './octokit.service'; diff --git a/packages/plugins/integration-github/src/octokit.service.ts b/packages/plugins/integration-github/src/octokit.service.ts new file mode 100644 index 00000000000..8b2888f5dd2 --- /dev/null +++ b/packages/plugins/integration-github/src/octokit.service.ts @@ -0,0 +1,143 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import * as chalk from 'chalk'; +import { App } from 'octokit'; +import { ResponseHeaders as OctokitResponseHeaders } from "@octokit/types"; +import { ModuleProviders, ProbotConfig } from './probot.types'; + +const GITHUB_API_VERSION = process.env.GAUZY_GITHUB_API_VERSION || '2022-11-28'; // Define a default version + +export interface OctokitResponse { + data: T; // The response data received from the GitHub API. + status: number; // The HTTP status code of the response (e.g., 200, 404, etc.). + headers: OctokitResponseHeaders; // The headers included in the response. + [key: string]: any; // Additional properties may be present depending on the specific response. +} + +@Injectable() +export class OctokitService { + + private readonly logger = new Logger('OctokitService'); + private readonly app: InstanceType | undefined; + + constructor( + @Inject(ModuleProviders.ProbotConfig) + private readonly config: ProbotConfig + ) { + /** */ + try { + if (this.config.appId && this.config.privateKey) { + this.app = new App({ + appId: this.config.appId, + privateKey: this.config.privateKey, + }); + console.log(chalk.green(`Octokit App successfully initialized.`)); + } else { + console.error(chalk.red(`Octokit App initialization failed: Missing appId or privateKey.`)); + } + } catch (error) { + console.error(chalk.red(`Octokit App initialization failed: ${error.message}`)); + } + } + + /** + * + * @returns + */ + getApp(): InstanceType | undefined { + return this.app; + } + + /** + * Get GitHub metadata for a specific installation. + * + * @param installation_id The installation ID for the GitHub App. + * @returns {Promise>} A promise that resolves with the GitHub metadata. + * @throws {Error} If the request to fetch metadata fails. + */ + public async getGithubInstallationMetadata(installation_id: number): Promise> { + if (!this.app) { + throw new Error('Octokit instance is not available.'); + } + try { + // Get an Octokit instance for the installation + const octokit = await this.app.getInstallationOctokit(installation_id); + + // Send a request to the GitHub API to get installation metadata + return await octokit.request('GET /app/installations/{installation_id}', { + installation_id, + headers: { + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } + }); + } catch (error) { + this.logger.error('Failed to fetch GitHub installation metadata', error.message); + throw new Error('Failed to fetch GitHub installation metadata'); + } + } + + /** + * Get GitHub repositories for a specific installation. + * + * @param installation_id The installation ID for the GitHub App. + * @returns {Promise>} A promise that resolves with the GitHub repositories. + * @throws {Error} If the request to fetch repositories fails. + */ + public async getGithubRepositories(installation_id: number): Promise> { + if (!this.app) { + throw new Error('Octokit instance is not available.'); + } + try { + // Get an Octokit instance for the installation + const octokit = await this.app.getInstallationOctokit(installation_id); + + // Send a request to the GitHub API to get repositories + return await octokit.request('GET /installation/repositories', { + installation_id, + headers: { + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } + }); + } catch (error) { + this.logger.error('Failed to fetch GitHub installation repositories', error.message); + throw new Error('Failed to fetch GitHub installation repositories'); + } + } + + /** + * Fetch GitHub repository issues for a given installation, owner, and repository. + * + * @param {number} installation_id - The installation ID for the GitHub app. + * @param {Object} options - Options object with 'owner' and 'repo' properties. + * @param {string} options.owner - The owner (username or organization) of the repository. + * @param {string} options.repo - The name of the repository. + * @returns {Promise>} A promise that resolves to the response from the GitHub API. + * @throws {Error} If the request to the GitHub API fails. + */ + public async getGithubRepositoryIssues(installation_id: number, { + owner, + repo + }: { + owner: string; + repo: string; + }): Promise> { + if (!this.app) { + throw new Error('Octokit instance is not available.'); + } + try { + // Get an Octokit instance for the installation + const octokit = await this.app.getInstallationOctokit(installation_id); + + // Send a request to the GitHub API to get repository issues + return await octokit.request('GET /repos/{owner}/{repo}/issues', { + owner: owner, + repo: repo, + headers: { + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } + }); + } catch (error) { + this.logger.error('Failed to fetch GitHub installation repository issues', error.message); + throw new Error('Failed to fetch GitHub installation repository issues'); + } + } +} diff --git a/packages/plugins/integration-github/src/probot.discovery.ts b/packages/plugins/integration-github/src/probot.discovery.ts new file mode 100644 index 00000000000..9477168c7ac --- /dev/null +++ b/packages/plugins/integration-github/src/probot.discovery.ts @@ -0,0 +1,222 @@ +import { DiscoveryService, MetadataScanner } from '@nestjs/core'; +import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; +import { + Inject, + Injectable, + Logger, + OnApplicationBootstrap, + OnApplicationShutdown, + OnModuleInit, +} from '@nestjs/common'; +import { Probot } from 'probot'; +import SmeeClient from 'smee-client'; +import * as _ from 'underscore'; +import * as chalk from 'chalk'; +import { v4 } from 'uuid'; +import { ModuleProviders, ProbotConfig } from './probot.types'; +import { createProbot, createSmee } from './probot.helpers'; +import { HookMetadataAccessor } from './hook-metadata.accessor'; + +@Injectable() +export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown { + + private readonly logger = new Logger('ProbotDiscovery'); + private readonly hooks: Map; + private smee: SmeeClient; + private readonly probot: Probot; + + constructor( + private readonly discoveryService: DiscoveryService, + private readonly metadataAccessor: HookMetadataAccessor, + private readonly metadataScanner: MetadataScanner, + @Inject(ModuleProviders.ProbotConfig) + private readonly config: ProbotConfig + ) { + this.hooks = new Map(); + /** */ + try { + if (this.config.appId && this.config.privateKey) { + this.probot = createProbot(this.config); + console.log(chalk.green(`Probot App successfully initialized.`)); + } else { + console.error(chalk.red(`Probot App initialization failed: Missing appId or privateKey.`)); + } + } catch (error) { + console.error(chalk.red(`Probot App initialization failed: ${error.message}`)); + } + } + + /** + * + */ + public async onModuleInit() { + this.discoverInstanceWrappers(); + } + + /** + * Implementation for onApplicationBootstrap + * This method is called when the application is fully initialized. + * You can perform setup tasks here. + */ + onApplicationBootstrap(): any { + // Check if webhookProxy is configured + if (!_.isEmpty(this.config.webhookProxy)) { + // Create and start a SmeeClient if webhookProxy is configured + this.smee = createSmee(this.config); + this.smee.start(); + } + + // Mount the webhook event listeners + this.mountHooks(); + } + + /** + * Implementation for onApplicationShutdown + * This method is called when the application is about to shut down. + * You can perform cleanup tasks here. + * @param signal + */ + onApplicationShutdown(signal?: string): any { + // TODO clear probot event handlers on shutdown + } + + /** + * Initialize and mount event listeners for Probot hooks. + */ + mountHooks() { + if (!this.probot) { + return; + } + this.probot + .load((app: { + on: (eventName: any, callback: (context: any) => Promise) => any; + }) => { + // Iterate through registered hooks and add event listeners + this.hooks.forEach((hook) => { + app.on( + hook.eventOrEvents, // The event name or names to listen for + this.initContext(hook.target) // The callback function for the event + ); + }); + }) + .then(() => { + // Log a message when hook event listeners are initialized + this.logger.log('Hook event listeners initialized'); + }) + .catch(this.logger.error); // Handle any errors that occur during initialization + } + + /** + * Create an asynchronous context wrapper for a function. + * @param fn The original function to be wrapped. + * @returns An asynchronous function that calls the original function. + */ + initContext(fn: (context: any) => any) { + return async (context: any) => { + await fn(context); // Call the original function with the provided context. + }; + } + + /** + * Explore and analyze methods of instance wrappers (controllers and providers). + */ + discoverInstanceWrappers() { + // Get all instance wrappers for controllers and providers + const instanceWrappers: InstanceWrapper[] = [ + ...this.discoveryService.getControllers(), + ...this.discoveryService.getProviders(), + ]; + + // Filter instance wrappers with static dependency trees + const staticInstanceWrappers = instanceWrappers.filter( + (wrapper: InstanceWrapper) => wrapper.isDependencyTreeStatic() + ); + + // Iterate through static instance wrappers and explore methods + staticInstanceWrappers.forEach((wrapper: InstanceWrapper) => { + const { instance } = wrapper; + + // Skip if instance or its prototype is missing + if (!instance || !Object.getPrototypeOf(instance)) { + return; + } + + // Get the prototype of the instance + const instancePrototype = Object.getPrototypeOf(instance); + + // Get all method names from the prototype + const methodNames = this.metadataScanner.getAllMethodNames(instancePrototype); + + // Iterate through method names and lookup hooks + methodNames.forEach((methodName: string) => { + this.lookupHooks(instance, methodName); + }); + }); + } + + /** + * Look up and process webhook hooks associated with a method of an instance. + * @param instance The instance to examine. + * @param key The method name to inspect. + * @returns The stored hook information or null if no webhook event definition. + */ + lookupHooks(instance: Record any>, key: string) { + // Get the method reference from the instance + const methodRef = instance[key]; + // Get webhook event metadata for the method + const hookMetadata = this.metadataAccessor.getWebhookEvents(methodRef); + // Wrap the method in try-catch blocks if needed + const hookFn = this.wrapFunctionInTryCatchBlocks(methodRef, instance); + + // If no webhook event definition, skip + if (_.isEmpty(hookMetadata)) { + return null; + } + + // Generate a unique key and store the hook information + return this.hooks.set(v4(), { + target: hookFn, + eventOrEvents: hookMetadata, + }); + } + + /** + * Wrap a method reference in try-catch blocks to handle errors and log them. + * @param methodRef The method reference to wrap. + * @param instance The instance to which the method belongs. + * @returns An asynchronous function that handles errors and logs them. + */ + private wrapFunctionInTryCatchBlocks( + methodRef: () => any, + instance: Record + ) { + // Return an asynchronous function that wraps the method reference + return async (...args: unknown[]) => { + try { + // Call the method reference with the provided instance and arguments + await methodRef.call(instance, ...args); + } catch (error) { + // Handle and log any errors using the logger + this.logger.error(error); + } + }; + } + + /** + * Receive and process a GitHub webhook request. + * @param request The incoming webhook request. + * @returns A promise that resolves when the webhook is processed. + */ + public receiveHook(request: any) { + if (!this.probot) { + return; + } + // Extract relevant information from the request + const id = request.headers['x-github-delivery'] as string; + const event = request.headers['x-github-event']; + const body = request.body; + + // Call the probot's receive method with extracted information + return this.probot.receive({ id, name: event, payload: body }); + } +} diff --git a/packages/plugins/integration-github/src/probot.helpers.ts b/packages/plugins/integration-github/src/probot.helpers.ts new file mode 100644 index 00000000000..da3dde942db --- /dev/null +++ b/packages/plugins/integration-github/src/probot.helpers.ts @@ -0,0 +1,72 @@ +import { getPrivateKey } from '@probot/get-private-key'; +import { Probot } from 'probot'; +import SmeeClient from 'smee-client'; +import { Octokit } from '@octokit/rest'; +import { createAppAuth } from '@octokit/auth-app'; +import { OctokitConfig, ProbotConfig } from './probot.types'; + +const GITHUB_API_URL = 'https://api.github.com'; + +/** + * Parse and restructure Probot configuration into a more organized format. + * @param config - Probot configuration. + * @returns Parsed configuration object. + */ +export const parseConfig = (config: ProbotConfig): Record => ({ + appId: config.appId, + privateKey: getPrivateKey({ env: { PRIVATE_KEY: config.privateKey ? config.privateKey.replace(/\\n/g, '\n') : '' } }) as string, + webhookSecret: config.webhookSecret, + ghUrl: config.ghUrl || GITHUB_API_URL, + webhookProxy: config.webhookProxy, + webhookPath: config.webhookPath, + clientId: config.clientId, + clientSecret: config.clientSecret +}); + +/** + * Create and configure a Probot instance. + * @param config - Probot configuration. + * @returns A configured Probot instance. + */ +export const createProbot = (config: ProbotConfig): Probot => { + const parsedConfig = parseConfig(config); + return new Probot({ + ...parsedConfig, // Spread the parsed configuration properties + }); +}; + +/** + * Create and configure a SmeeClient instance. + * @param config - Probot configuration. + * @returns A configured SmeeClient instance. + */ +export const createSmee = (config: ProbotConfig): SmeeClient => { + const parsedConfig = parseConfig(config); + return new SmeeClient({ + source: parsedConfig.webhookProxy as string, + target: parsedConfig.webhookPath as string, + logger: console, + }); +}; + +/** + * Create and configure an Octokit instance for GitHub API requests. + * @param config - Configuration options for Octokit. + * @returns An Octokit instance. + */ +export const createOctokit = (config: OctokitConfig): Octokit => { + /** Parsed Probot Config */ + const probot = parseConfig(config.probot); + /** return an Octokit instance. */ + return new Octokit({ + authStrategy: createAppAuth, + baseUrl: probot.ghUrl, + auth: { + appId: probot.appId, + privateKey: probot.privateKey, + clientId: probot.clientId, + clientSecret: probot.clientSecret, + ...config.auth, // Include other auth options if needed + }, + }); +}; diff --git a/packages/plugins/integration-github/src/probot.module.ts b/packages/plugins/integration-github/src/probot.module.ts new file mode 100644 index 00000000000..9669a05b967 --- /dev/null +++ b/packages/plugins/integration-github/src/probot.module.ts @@ -0,0 +1,65 @@ +import { DiscoveryModule } from '@nestjs/core'; +import { DynamicModule, Module } from '@nestjs/common'; +import { + ProbotModuleOptions, + ModuleProviders, + ProbotModuleAsyncOptions, +} from './probot.types'; +import { ProbotDiscovery } from './probot.discovery'; +import { getControllerClass } from './hook.controller'; +import { HookMetadataAccessor } from './hook-metadata.accessor'; +import { OctokitService } from './octokit.service'; + +@Module({ + imports: [DiscoveryModule] +}) +export class ProbotModule { + /** + * Register the Probot module. + * @param options - Configuration options for the Probot module. + * @returns A dynamic module configuration. + */ + static forRoot(options: ProbotModuleOptions): DynamicModule { + const HookController = getControllerClass({ path: options.path }); + return { + global: options.isGlobal || true, + module: ProbotModule, + controllers: [HookController], + providers: [ + { + provide: ModuleProviders.ProbotConfig, + useFactory: () => options.config, + }, + HookMetadataAccessor, + ProbotDiscovery, + OctokitService, + ], + exports: [OctokitService], + }; + } + + /** + * Register the Probot module asynchronously. + * @param options - Configuration options for the Probot module. + * @returns A dynamic module configuration. + */ + static forRootAsync(options: ProbotModuleAsyncOptions): DynamicModule { + const HookController = getControllerClass({ path: options.path }); + return { + module: ProbotModule, + global: options.isGlobal || true, + controllers: [HookController], + providers: [ + { + provide: ModuleProviders.ProbotConfig, + useFactory: options.useFactory, + inject: options.inject || [], + }, + HookMetadataAccessor, + ProbotDiscovery, + OctokitService, + ], + exports: [OctokitService], + }; + } +} diff --git a/packages/plugins/integration-github/src/probot.types.ts b/packages/plugins/integration-github/src/probot.types.ts new file mode 100644 index 00000000000..f1f528d447d --- /dev/null +++ b/packages/plugins/integration-github/src/probot.types.ts @@ -0,0 +1,53 @@ +import { ModuleMetadata } from '@nestjs/common'; + +// Define interfaces for Probot and Octokit configuration + +export interface ProbotConfig { + // GitHub App configuration options + appId: string; + privateKey: string; + webhookSecret?: string; + webhookPath?: string; + ghUrl?: string; + clientId: string; + clientSecret: string; + webhookProxy?: string; +} + +export interface OctokitConfig { + // Octokit library configuration options + auth?: Record; + probot: ProbotConfig; +} + +// Define options for the Probot module + +export interface ProbotModuleOptions { + // Specifies if the Probot module should be global + isGlobal?: boolean; + // The path at which the module is mounted + path: string; + // Probot configuration options + config: ProbotConfig; +} + +export interface ProbotModuleAsyncOptions extends Pick { + // Specifies if the Probot module should be global + isGlobal?: boolean; + // The path at which the module is mounted + path: string; + // Factory function to asynchronously provide Probot configuration + useFactory: (...args: any[]) => Promise | ProbotConfig; + // Optional list of dependencies to inject into the factory function + inject?: any[]; +} + +// Define enums for metadata and module providers +export enum ProbotMetadata { + name = 'probot/metadata/hook', +} + +export enum ModuleProviders { + // Provider key for Probot configuration + ProbotConfig = 'probot/provider/config', +} diff --git a/yarn.lock b/yarn.lock index 8ec937d0d1c..3c34fa8e565 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3067,6 +3067,11 @@ dependencies: "@hapi/hoek" "9.x.x" +"@hapi/bourne@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.1.0.tgz#66aff77094dc3080bd5df44ec63881f2676eb020" + integrity sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q== + "@hapi/hoek@9.x.x": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -5475,6 +5480,13 @@ webpack-merge "^5.8.0" webpack-node-externals "^3.0.0" +"@nrwl/nx-cloud@16.3.0", "@nrwl/nx-cloud@^16.3.0": + version "16.3.0" + resolved "https://registry.yarnpkg.com/@nrwl/nx-cloud/-/nx-cloud-16.3.0.tgz#0f1d563200af5bb6ce51a8408d698774d5ccdbbd" + integrity sha512-nJrGsVufhY74KcP7kM7BqFOGAoO5OEF6+wfiM295DgmEG9c1yW+x5QiQaC42K9SWYn/eKQa1X7466ZA5lynXoQ== + dependencies: + nx-cloud "16.3.0" + "@nrwl/storybook@13.10.6": version "13.10.6" resolved "https://registry.yarnpkg.com/@nrwl/storybook/-/storybook-13.10.6.tgz#087cfcac1104463938dbaa5b6ecd1ce78d7b692c" @@ -5833,6 +5845,69 @@ resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== +"@octokit/app@^13.1.5": + version "13.1.8" + resolved "https://registry.yarnpkg.com/@octokit/app/-/app-13.1.8.tgz#9e43e7e1ffc8f028130cabdf587cbacccf0c0257" + integrity sha512-bCncePMguVyFpdBbnceFKfmPOuUD94T189GuQ0l00ZcQ+mX4hyPqnaWJlsXE2HSdA71eV7p8GPDZ+ErplTkzow== + dependencies: + "@octokit/auth-app" "^4.0.13" + "@octokit/auth-unauthenticated" "^3.0.0" + "@octokit/core" "^4.0.0" + "@octokit/oauth-app" "^4.0.7" + "@octokit/plugin-paginate-rest" "^6.0.0" + "@octokit/types" "^9.0.0" + "@octokit/webhooks" "^10.0.0" + +"@octokit/auth-app@^4.0.13", "@octokit/auth-app@^4.0.2": + version "4.0.13" + resolved "https://registry.yarnpkg.com/@octokit/auth-app/-/auth-app-4.0.13.tgz#53323bee6bfefbb73ea544dd8e6a0144550e13e3" + integrity sha512-NBQkmR/Zsc+8fWcVIFrwDgNXS7f4XDrkd9LHdi9DPQw1NdGHLviLzRO2ZBwTtepnwHXW5VTrVU9eFGijMUqllg== + dependencies: + "@octokit/auth-oauth-app" "^5.0.0" + "@octokit/auth-oauth-user" "^2.0.0" + "@octokit/request" "^6.0.0" + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^9.0.0" + deprecation "^2.3.1" + lru-cache "^9.0.0" + universal-github-app-jwt "^1.1.1" + universal-user-agent "^6.0.0" + +"@octokit/auth-oauth-app@^5.0.0": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@octokit/auth-oauth-app/-/auth-oauth-app-5.0.6.tgz#e5f922623eb261485efc87f5d0d5b509c71caec8" + integrity sha512-SxyfIBfeFcWd9Z/m1xa4LENTQ3l1y6Nrg31k2Dcb1jS5ov7pmwMJZ6OGX8q3K9slRgVpeAjNA1ipOAMHkieqyw== + dependencies: + "@octokit/auth-oauth-device" "^4.0.0" + "@octokit/auth-oauth-user" "^2.0.0" + "@octokit/request" "^6.0.0" + "@octokit/types" "^9.0.0" + "@types/btoa-lite" "^1.0.0" + btoa-lite "^1.0.0" + universal-user-agent "^6.0.0" + +"@octokit/auth-oauth-device@^4.0.0": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-oauth-device/-/auth-oauth-device-4.0.5.tgz#21e981f51ae63d419ca3db0b75e32c85b33fa0da" + integrity sha512-XyhoWRTzf2ZX0aZ52a6Ew5S5VBAfwwx1QnC2Np6Et3MWQpZjlREIcbcvVZtkNuXp6Z9EeiSLSDUqm3C+aMEHzQ== + dependencies: + "@octokit/oauth-methods" "^2.0.0" + "@octokit/request" "^6.0.0" + "@octokit/types" "^9.0.0" + universal-user-agent "^6.0.0" + +"@octokit/auth-oauth-user@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-oauth-user/-/auth-oauth-user-2.1.2.tgz#7091e1b29527e577b16d0f1699d49fe3d39946ff" + integrity sha512-kkRqNmFe7s5GQcojE3nSlF+AzYPpPv7kvP/xYEnE57584pixaFBH8Vovt+w5Y3E4zWUEOxjdLItmBTFAWECPAg== + dependencies: + "@octokit/auth-oauth-device" "^4.0.0" + "@octokit/oauth-methods" "^2.0.0" + "@octokit/request" "^6.0.0" + "@octokit/types" "^9.0.0" + btoa-lite "^1.0.0" + universal-user-agent "^6.0.0" + "@octokit/auth-token@^2.4.4": version "2.5.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" @@ -5840,7 +5915,20 @@ dependencies: "@octokit/types" "^6.0.3" -"@octokit/core@^3.5.1": +"@octokit/auth-token@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.4.tgz#70e941ba742bdd2b49bdb7393e821dea8520a3db" + integrity sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ== + +"@octokit/auth-unauthenticated@^3.0.0": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-unauthenticated/-/auth-unauthenticated-3.0.5.tgz#a562bffd6ca0d0e80541eaf9f9b89b8d53020228" + integrity sha512-yH2GPFcjrTvDWPwJWWCh0tPPtTL5SMgivgKPA+6v/XmYN6hGQkAto8JtZibSKOpf8ipmeYhLNWQ2UgW0GYILCw== + dependencies: + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^9.0.0" + +"@octokit/core@^3.2.4", "@octokit/core@^3.5.1": version "3.6.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== @@ -5853,6 +5941,19 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" +"@octokit/core@^4.0.0", "@octokit/core@^4.2.1": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.2.4.tgz#d8769ec2b43ff37cc3ea89ec4681a20ba58ef907" + integrity sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ== + dependencies: + "@octokit/auth-token" "^3.0.0" + "@octokit/graphql" "^5.0.0" + "@octokit/request" "^6.0.0" + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^9.0.0" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + "@octokit/endpoint@^6.0.1": version "6.0.12" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" @@ -5862,6 +5963,15 @@ is-plain-object "^5.0.0" universal-user-agent "^6.0.0" +"@octokit/endpoint@^7.0.0": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.6.tgz#791f65d3937555141fb6c08f91d618a7d645f1e2" + integrity sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg== + dependencies: + "@octokit/types" "^9.0.0" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + "@octokit/graphql@^4.5.8": version "4.8.0" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" @@ -5871,29 +5981,95 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" +"@octokit/graphql@^5.0.0": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.6.tgz#9eac411ac4353ccc5d3fca7d76736e6888c5d248" + integrity sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw== + dependencies: + "@octokit/request" "^6.0.0" + "@octokit/types" "^9.0.0" + universal-user-agent "^6.0.0" + +"@octokit/oauth-app@^4.0.7", "@octokit/oauth-app@^4.2.1": + version "4.2.4" + resolved "https://registry.yarnpkg.com/@octokit/oauth-app/-/oauth-app-4.2.4.tgz#d385ffebe116c684940bf255a2189665c61ee5a0" + integrity sha512-iuOVFrmm5ZKNavRtYu5bZTtmlKLc5uVgpqTfMEqYYf2OkieV6VdxKZAb5qLVdEPL8LU2lMWcGpavPBV835cgoA== + dependencies: + "@octokit/auth-oauth-app" "^5.0.0" + "@octokit/auth-oauth-user" "^2.0.0" + "@octokit/auth-unauthenticated" "^3.0.0" + "@octokit/core" "^4.0.0" + "@octokit/oauth-authorization-url" "^5.0.0" + "@octokit/oauth-methods" "^2.0.0" + "@types/aws-lambda" "^8.10.83" + fromentries "^1.3.1" + universal-user-agent "^6.0.0" + +"@octokit/oauth-authorization-url@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@octokit/oauth-authorization-url/-/oauth-authorization-url-5.0.0.tgz#029626ce87f3b31addb98cd0d2355c2381a1c5a1" + integrity sha512-y1WhN+ERDZTh0qZ4SR+zotgsQUE1ysKnvBt1hvDRB2WRzYtVKQjn97HEPzoehh66Fj9LwNdlZh+p6TJatT0zzg== + +"@octokit/oauth-methods@^2.0.0": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@octokit/oauth-methods/-/oauth-methods-2.0.6.tgz#3a089781e90171cbe8a0efa448a6a60229bdd3fb" + integrity sha512-l9Uml2iGN2aTWLZcm8hV+neBiFXAQ9+3sKiQe/sgumHlL6HDg0AQ8/l16xX/5jJvfxueqTW5CWbzd0MjnlfHZw== + dependencies: + "@octokit/oauth-authorization-url" "^5.0.0" + "@octokit/request" "^6.2.3" + "@octokit/request-error" "^3.0.3" + "@octokit/types" "^9.0.0" + btoa-lite "^1.0.0" + "@octokit/openapi-types@^12.11.0": version "12.11.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== +"@octokit/openapi-types@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a" + integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw== + +"@octokit/openapi-types@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.0.0.tgz#f43d765b3c7533fd6fb88f3f25df079c24fccf69" + integrity sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw== + +"@octokit/plugin-enterprise-compatibility@^1.2.8": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-compatibility/-/plugin-enterprise-compatibility-1.3.0.tgz#034f035cc1789b0f0d616e71e41f50f73804e89e" + integrity sha512-h34sMGdEOER/OKrZJ55v26ntdHb9OPfR1fwOx6Q4qYyyhWA104o11h9tFxnS/l41gED6WEI41Vu2G2zHDVC5lQ== + dependencies: + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.0.3" + "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== -"@octokit/plugin-paginate-rest@^2.16.8": +"@octokit/plugin-paginate-rest@^2.16.8", "@octokit/plugin-paginate-rest@^2.6.2": version "2.21.3" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz#7f12532797775640dbb8224da577da7dc210c87e" integrity sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw== dependencies: "@octokit/types" "^6.40.0" +"@octokit/plugin-paginate-rest@^6.0.0", "@octokit/plugin-paginate-rest@^6.1.0": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz#f86456a7a1fe9e58fec6385a85cf1b34072341f8" + integrity sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ== + dependencies: + "@octokit/tsconfig" "^1.0.2" + "@octokit/types" "^9.2.3" + "@octokit/plugin-request-log@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== -"@octokit/plugin-rest-endpoint-methods@^5.12.0": +"@octokit/plugin-rest-endpoint-methods@^5.0.1", "@octokit/plugin-rest-endpoint-methods@^5.12.0": version "5.16.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz#7ee8bf586df97dd6868cf68f641354e908c25342" integrity sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw== @@ -5901,7 +6077,46 @@ "@octokit/types" "^6.39.0" deprecation "^2.3.1" -"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": +"@octokit/plugin-rest-endpoint-methods@^7.1.1": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz#37a84b171a6cb6658816c82c4082ac3512021797" + integrity sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA== + dependencies: + "@octokit/types" "^10.0.0" + +"@octokit/plugin-retry@^3.0.6": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz#ae625cca1e42b0253049102acd71c1d5134788fe" + integrity sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ== + dependencies: + "@octokit/types" "^6.0.3" + bottleneck "^2.15.3" + +"@octokit/plugin-retry@^4.1.3": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-4.1.6.tgz#e33b1e520f0bd24d515c9901676b55df64dfc795" + integrity sha512-obkYzIgEC75r8+9Pnfiiqy3y/x1bc3QLE5B7qvv9wi9Kj0R5tGQFC6QMBg1154WQ9lAVypuQDGyp3hNpp15gQQ== + dependencies: + "@octokit/types" "^9.0.0" + bottleneck "^2.15.3" + +"@octokit/plugin-throttling@^3.3.4": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-throttling/-/plugin-throttling-3.7.0.tgz#a35cd05de22b2ef13fde45390d983ff8365b9a9e" + integrity sha512-qrKT1Yl/KuwGSC6/oHpLBot3ooC9rq0/ryDYBCpkRtoj+R8T47xTMDT6Tk2CxWopFota/8Pi/2SqArqwC0JPow== + dependencies: + "@octokit/types" "^6.0.1" + bottleneck "^2.15.3" + +"@octokit/plugin-throttling@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-throttling/-/plugin-throttling-5.2.3.tgz#9f552a14dcee5c7326dd9dee64a71ea76b108814" + integrity sha512-C9CFg9mrf6cugneKiaI841iG8DOv6P5XXkjmiNNut+swePxQ7RWEdAZRp5rJoE1hjsIqiYcKa/ZkOQ+ujPI39Q== + dependencies: + "@octokit/types" "^9.0.0" + bottleneck "^2.15.3" + +"@octokit/request-error@^2.0.2", "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== @@ -5910,6 +6125,15 @@ deprecation "^2.0.0" once "^1.4.0" +"@octokit/request-error@^3.0.0", "@octokit/request-error@^3.0.3", "@octokit/request-error@^v3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.3.tgz#ef3dd08b8e964e53e55d471acfe00baa892b9c69" + integrity sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ== + dependencies: + "@octokit/types" "^9.0.0" + deprecation "^2.0.0" + once "^1.4.0" + "@octokit/request@^5.6.0", "@octokit/request@^5.6.3": version "5.6.3" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" @@ -5922,6 +6146,18 @@ node-fetch "^2.6.7" universal-user-agent "^6.0.0" +"@octokit/request@^6.0.0", "@octokit/request@^6.2.3": + version "6.2.8" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.8.tgz#aaf480b32ab2b210e9dadd8271d187c93171d8eb" + integrity sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw== + dependencies: + "@octokit/endpoint" "^7.0.0" + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^9.0.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.7" + universal-user-agent "^6.0.0" + "@octokit/rest@^18.0.0", "@octokit/rest@^18.1.0": version "18.12.0" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.12.0.tgz#f06bc4952fc87130308d810ca9d00e79f6988881" @@ -5932,13 +6168,79 @@ "@octokit/plugin-request-log" "^1.0.4" "@octokit/plugin-rest-endpoint-methods" "^5.12.0" -"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": +"@octokit/tsconfig@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@octokit/tsconfig/-/tsconfig-1.0.2.tgz#59b024d6f3c0ed82f00d08ead5b3750469125af7" + integrity sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA== + +"@octokit/types@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-10.0.0.tgz#7ee19c464ea4ada306c43f1a45d444000f419a4a" + integrity sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg== + dependencies: + "@octokit/openapi-types" "^18.0.0" + +"@octokit/types@^6.0.1", "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": version "6.41.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04" integrity sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg== dependencies: "@octokit/openapi-types" "^12.11.0" +"@octokit/types@^8.0.0": + version "8.2.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.2.1.tgz#a6de091ae68b5541f8d4fcf9a12e32836d4648aa" + integrity sha512-8oWMUji8be66q2B9PmEIUyQm00VPDPun07umUWSaCwxmeaquFBro4Hcc3ruVoDo3zkQyZBlRvhIMEYS3pBhanw== + dependencies: + "@octokit/openapi-types" "^14.0.0" + +"@octokit/types@^9.0.0", "@octokit/types@^9.2.2", "@octokit/types@^9.2.3": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-9.3.2.tgz#3f5f89903b69f6a2d196d78ec35f888c0013cac5" + integrity sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA== + dependencies: + "@octokit/openapi-types" "^18.0.0" + +"@octokit/webhooks-methods@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@octokit/webhooks-methods/-/webhooks-methods-2.0.0.tgz#1108b9ea661ca6c81e4a8bfa63a09eb27d5bc2db" + integrity sha512-35cfQ4YWlnZnmZKmIxlGPUPLtbkF8lr/A/1Sk1eC0ddLMwQN06dOuLc+dI3YLQS+T+MoNt3DIQ0NynwgKPilig== + +"@octokit/webhooks-methods@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@octokit/webhooks-methods/-/webhooks-methods-3.0.3.tgz#2648668d34fe44e437eca90c9031d0f3cb759c77" + integrity sha512-2vM+DCNTJ5vL62O5LagMru6XnYhV4fJslK+5YUkTa6rWlW2S+Tqs1lF9Wr9OGqHfVwpBj3TeztWfVON/eUoW1Q== + +"@octokit/webhooks-types@5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@octokit/webhooks-types/-/webhooks-types-5.8.0.tgz#b76d1a3e3ad82cec5680d3c6c3443a620047a6ef" + integrity sha512-8adktjIb76A7viIdayQSFuBEwOzwhDC+9yxZpKNHjfzrlostHCw0/N7JWpWMObfElwvJMk2fY2l1noENCk9wmw== + +"@octokit/webhooks-types@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@octokit/webhooks-types/-/webhooks-types-6.11.0.tgz#1fb903bff3f2883490d6ba88d8cb8f8a55f68176" + integrity sha512-AanzbulOHljrku1NGfafxdpTCfw2ENaWzH01N2vqQM+cUFbk868Cgh0xylz0JIM9BoKbfI++bdD6EYX0Q/UTEw== + +"@octokit/webhooks@^10.0.0": + version "10.9.1" + resolved "https://registry.yarnpkg.com/@octokit/webhooks/-/webhooks-10.9.1.tgz#4674a6924567419d7d0187a8b6c88ec468a97a86" + integrity sha512-5NXU4VfsNOo2VSU/SrLrpPH2Z1ZVDOWFcET4EpnEBX1uh/v8Uz65UVuHIRx5TZiXhnWyRE9AO1PXHa+M/iWwZA== + dependencies: + "@octokit/request-error" "^3.0.0" + "@octokit/webhooks-methods" "^3.0.0" + "@octokit/webhooks-types" "6.11.0" + aggregate-error "^3.1.0" + +"@octokit/webhooks@^9.8.4": + version "9.26.0" + resolved "https://registry.yarnpkg.com/@octokit/webhooks/-/webhooks-9.26.0.tgz#cf453bb313da3b66f1a90c84464d978e1c625cce" + integrity sha512-foZlsgrTDwAmD5j2Czn6ji10lbWjGDVsUxTIydjG9KTkAWKJrFapXJgO5SbGxRwfPd3OJdhK3nA2YPqVhxLXqA== + dependencies: + "@octokit/request-error" "^2.0.2" + "@octokit/webhooks-methods" "^2.0.0" + "@octokit/webhooks-types" "5.8.0" + aggregate-error "^3.1.0" + "@opencensus/core@0.0.9": version "0.0.9" resolved "https://registry.yarnpkg.com/@opencensus/core/-/core-0.0.9.tgz#b16f775435ee309433e4126af194d37313fc93b3" @@ -6056,6 +6358,33 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== +"@probot/get-private-key@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@probot/get-private-key/-/get-private-key-1.1.1.tgz#12bf61d00a15760d9b0bd713a794f9c4ba4ad5d3" + integrity sha512-hOmBNSAhSZc6PaNkTvj6CO9R5J67ODJ+w5XQlDW9w/6mtcpHWK4L+PZcW0YwVM7PpetLZjN6rsKQIR9yqIaWlA== + dependencies: + "@types/is-base64" "^1.1.0" + is-base64 "^1.1.0" + +"@probot/octokit-plugin-config@^1.0.0": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@probot/octokit-plugin-config/-/octokit-plugin-config-1.1.6.tgz#c450a746f082c8ec9b6d1a481a71778f7720fa9b" + integrity sha512-L29wmnFvilzSfWn9tUgItxdLv0LJh2ICjma3FmLr80Spu3wZ9nHyRrKMo9R5/K2m7VuWmgoKnkgRt2zPzAQBEQ== + dependencies: + "@types/js-yaml" "^4.0.5" + js-yaml "^4.1.0" + +"@probot/pino@^2.2.0": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@probot/pino/-/pino-2.3.5.tgz#f1d051edfc080c9183592e90ac8ae03c14e3951a" + integrity sha512-IiyiNZonMw1dHC4EAdD55y5owV733d9Gll/IKsrLikB7EJ54+eMCOtL/qo+OmgWN9XV3NTDfziEQF2og/OBKog== + dependencies: + "@sentry/node" "^6.0.0" + pino-pretty "^6.0.0" + pump "^3.0.0" + readable-stream "^3.6.0" + split2 "^4.0.0" + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -6400,6 +6729,17 @@ "@sentry/utils" "7.68.0" tslib "^2.4.1 || ^1.9.3" +"@sentry/core@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785" + integrity sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw== + dependencies: + "@sentry/hub" "6.19.7" + "@sentry/minimal" "6.19.7" + "@sentry/types" "6.19.7" + "@sentry/utils" "6.19.7" + tslib "^1.9.3" + "@sentry/core@7.63.0": version "7.63.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.63.0.tgz#8c38da6ef3a1de6e364463a09bc703b196ecbba4" @@ -6432,6 +6772,15 @@ lru_map "^0.3.3" tslib "^2.5.0" +"@sentry/hub@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.19.7.tgz#58ad7776bbd31e9596a8ec46365b45cd8b9cfd11" + integrity sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA== + dependencies: + "@sentry/types" "6.19.7" + "@sentry/utils" "6.19.7" + tslib "^1.9.3" + "@sentry/hub@^7.68.0": version "7.68.0" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.68.0.tgz#f317877a1d2819e9426696bfc0ba0c7cbe608e29" @@ -6442,6 +6791,15 @@ "@sentry/utils" "7.68.0" tslib "^2.4.1 || ^1.9.3" +"@sentry/minimal@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.19.7.tgz#b3ee46d6abef9ef3dd4837ebcb6bdfd01b9aa7b4" + integrity sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ== + dependencies: + "@sentry/hub" "6.19.7" + "@sentry/types" "6.19.7" + tslib "^1.9.3" + "@sentry/node@7.63.0": version "7.63.0" resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.63.0.tgz#38508a440c04c0e98d00f5a1855e5448ee70c8d6" @@ -6456,6 +6814,20 @@ lru_map "^0.3.3" tslib "^2.4.1 || ^1.9.3" +"@sentry/node@^6.0.0": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.19.7.tgz#32963b36b48daebbd559e6f13b1deb2415448592" + integrity sha512-gtmRC4dAXKODMpHXKfrkfvyBL3cI8y64vEi3fDD046uqYcrWdgoQsffuBbxMAizc6Ez1ia+f0Flue6p15Qaltg== + dependencies: + "@sentry/core" "6.19.7" + "@sentry/hub" "6.19.7" + "@sentry/types" "6.19.7" + "@sentry/utils" "6.19.7" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + "@sentry/node@^7.68.0": version "7.68.0" resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.68.0.tgz#da3407ba7455109cf625c9a134a3f74ec5f5ca6b" @@ -6495,6 +6867,11 @@ dependencies: "@sentry-internal/tracing" "7.68.0" +"@sentry/types@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7" + integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg== + "@sentry/types@7.63.0": version "7.63.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.63.0.tgz#8032029fee6f70e04b667646626a674b03e2f79b" @@ -6505,6 +6882,14 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.68.0.tgz#6134511106eed90bf033dc2ce76955f61582b48f" integrity sha512-5J2pH1Pjx/029zTm3CNY9MaE8Aui81nG7JCtlMp7uEfQ//9Ja4d4Sliz/kV4ARbkIKUZerSgaRAm3xCy5XOXLg== +"@sentry/utils@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79" + integrity sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA== + dependencies: + "@sentry/types" "6.19.7" + tslib "^1.9.3" + "@sentry/utils@7.63.0": version "7.63.0" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.63.0.tgz#7c598553b4dbb6e3740dc96bc7f112ec32edbe69" @@ -6748,6 +7133,11 @@ resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.18.tgz#3d93dde6eab654f7bc23e549d9af5d3fa4a5bdc5" integrity sha512-/IsuXp3B9R//uRLi40VlIYoMp7OzhkunPe2fDu7jGfQXI9y3CDCx6FC4juRLSqrpmLst3vgsiK536AAGJFl4Ww== +"@types/aws-lambda@^8.10.83": + version "8.10.121" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.121.tgz#2702b77a77fadea98bbef43502a289882361854b" + integrity sha512-Y/jsUwO18HuC0a39BuMQkSOd/kMGATh/h5LNksw8FlTafbQ3Ge3578ZoT8w8gSOsWl2qH1p/SS/R61vc0X5jIQ== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7": version "7.20.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" @@ -6803,6 +7193,11 @@ dependencies: "@types/node" "*" +"@types/btoa-lite@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/btoa-lite/-/btoa-lite-1.0.0.tgz#e190a5a548e0b348adb0df9ac7fa5f1151c7cca4" + integrity sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg== + "@types/cacheable-request@^6.0.1": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" @@ -6950,7 +7345,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@^4.17.13": +"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.9": version "4.17.17" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== @@ -7065,6 +7460,18 @@ resolved "https://registry.yarnpkg.com/@types/i18n/-/i18n-0.12.0.tgz#ea4e247c2da05a35002d7535d339c912c4513549" integrity sha512-3OlOD518lF2IQmZndRvoO5ZJzgUowLPIRMH4T8LTjTkgbZwZlKMR52/KLml0dJN/Bt6e9R10NxzjOQnDG2DDgg== +"@types/ioredis@^4.27.1": + version "4.28.10" + resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff" + integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ== + dependencies: + "@types/node" "*" + +"@types/is-base64@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/is-base64/-/is-base64-1.1.1.tgz#a17d2b0075f637f80f9ab5f76f0071a65f6965d4" + integrity sha512-JgnGhP+MeSHEQmvxcobcwPEP4Ew56voiq9/0hmP/41lyQ/3gBw/ZCIRy2v+QkEOdeCl58lRcrf6+Y6WMlJGETA== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -7100,6 +7507,11 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" +"@types/js-yaml@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -7117,6 +7529,13 @@ dependencies: "@types/node" "*" +"@types/jsonwebtoken@^9.0.0": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#9eeb56c76dd555039be2a3972218de5bd3b8d83e" + integrity sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q== + dependencies: + "@types/node" "*" + "@types/keyv@^3.1.4": version "3.1.4" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" @@ -7310,6 +7729,37 @@ dependencies: "@types/express" "*" +"@types/pino-http@^5.0.6": + version "5.8.1" + resolved "https://registry.yarnpkg.com/@types/pino-http/-/pino-http-5.8.1.tgz#ebb194750ad2f9245c3028b5d2c4e6d64f685ba9" + integrity sha512-A9MW6VCnx5ii7s+Fs5aFIw+aSZcBCpsZ/atpxamu8tTsvWFacxSf2Hrn1Ohn1jkVRB/LiPGOapRXcFawDBnDnA== + dependencies: + "@types/pino" "6.3" + +"@types/pino-pretty@*": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/pino-pretty/-/pino-pretty-5.0.0.tgz#aa7a61cfd553b051764acfa0a49872f7a09a1722" + integrity sha512-N1uzqSzioqz8R3AkDbSJwcfDWeI3YMPNapSQQhnB2ISU4NYgUIcAh+hYT5ygqBM+klX4htpEhXMmoJv3J7GrdA== + dependencies: + pino-pretty "*" + +"@types/pino-std-serializers@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1e28b80b554c8222858e99a4e0fc77fd070e10e8" + integrity sha512-gXfUZx2xIBbFYozGms53fT0nvkacx/+62c8iTxrEqH5PkIGAQvDbXg2774VWOycMPbqn5YJBQ3BMsg4Li3dWbg== + dependencies: + pino-std-serializers "*" + +"@types/pino@6.3", "@types/pino@^6.3.4": + version "6.3.12" + resolved "https://registry.yarnpkg.com/@types/pino/-/pino-6.3.12.tgz#4425db6ced806109c3df957100cba9dfcd73c228" + integrity sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw== + dependencies: + "@types/node" "*" + "@types/pino-pretty" "*" + "@types/pino-std-serializers" "*" + sonic-boom "^2.1.0" + "@types/plist@^3.0.1": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" @@ -7850,6 +8300,13 @@ abbrev@1, abbrev@^1.0.0, abbrev@~1.1.1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accept-language-parser@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791" @@ -8002,7 +8459,7 @@ agentkeepalive@^4.1.0, agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: depd "^2.0.0" humanize-ms "^1.2.1" -aggregate-error@^3.0.0: +aggregate-error@^3.0.0, aggregate-error@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== @@ -8545,6 +9002,16 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +args@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/args/-/args-5.0.3.tgz#943256db85021a85684be2f0882f25d796278702" + integrity sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA== + dependencies: + camelcase "5.0.0" + chalk "2.4.2" + leven "2.1.0" + mri "1.1.4" + argv-formatter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/argv-formatter/-/argv-formatter-1.0.0.tgz#a0ca0cbc29a5b73e836eebe1cbf6c5e0e4eb82f9" @@ -8971,6 +9438,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + atomically@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe" @@ -9045,6 +9517,15 @@ axe-core@3.5.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== +axios@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35" + integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axios@^0.21.0: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -9668,7 +10149,7 @@ bootstrap@^4.3.1: resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479" integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ== -bottleneck@^2.18.1: +bottleneck@^2.15.3, bottleneck@^2.18.1: version "2.19.5" resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== @@ -10035,6 +10516,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +btoa-lite@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" + integrity sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA== + buffer-alloc-unsafe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" @@ -10475,6 +10961,11 @@ camelcase-keys@^8.0.2: quick-lru "^6.1.1" type-fest "^2.13.0" +camelcase@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + camelcase@^2.0.0, camelcase@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -10581,6 +11072,15 @@ chalk@1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@3.0.0, chalk@^3.0.0, chalk@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -10616,15 +11116,6 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -11177,6 +11668,11 @@ cls-hooked@^4.2.2: emitter-listener "^1.0.1" semver "^5.4.1" +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + cmd-shim@^3.0.0, cmd-shim@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-3.0.3.tgz#2c35238d3df37d98ecdd7d5f6b8dc6b21cadc7cb" @@ -11318,11 +11814,16 @@ colorette@2.0.19, colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, color resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -colorette@^1.1.0: +colorette@^1.1.0, colorette@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== +colorette@^2.0.7: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -11949,7 +12450,7 @@ cookie@0.5.0, cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookiejar@^2.1.4: +cookiejar@^2.1.3, cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== @@ -13404,6 +13905,11 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dateformat@^4.5.1, dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + dayjs@^1.10.4, dayjs@^1.10.6: version "1.11.7" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" @@ -13724,7 +14230,7 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -denque@^1.5.0: +denque@^1.1.0, denque@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== @@ -15738,6 +16244,11 @@ event-stream@4.0.1: stream-combiner "^0.2.2" through "^2.3.8" +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter-asyncresource@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" @@ -15783,6 +16294,16 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.2.tgz#bc75ae1c60209e7cb1541231980460343eaea7c2" + integrity sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA== + +eventsource@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508" + integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -15981,6 +16502,15 @@ express-handlebars@^3.0.0: object.assign "^4.1.0" promise "^8.0.2" +express-handlebars@^6.0.3: + version "6.0.7" + resolved "https://registry.yarnpkg.com/express-handlebars/-/express-handlebars-6.0.7.tgz#f779254664eff0e250362ef1c2b30587059c212a" + integrity sha512-iYeMFpc/hMD+E6FNAZA5fgWeXnXr4rslOSPkeEV6TwdmpJ5lEXuWX0u9vFYs31P2MURctQq2batR09oeNj0LIg== + dependencies: + glob "^8.1.0" + graceful-fs "^4.2.10" + handlebars "^4.7.7" + express-session@^1.17.2: version "1.17.3" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.3.tgz#14b997a15ed43e5949cb1d073725675dd2777f36" @@ -16152,6 +16682,11 @@ fancy-log@^2.0.0: dependencies: color-support "^1.1.3" +fast-copy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" + integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -16234,7 +16769,12 @@ fast-printf@^1.6.9: dependencies: boolean "^3.1.4" -fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.1.1: +fast-redact@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.3.0.tgz#7c83ce3a7be4898241a46560d51de10f653f7634" + integrity sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ== + +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -16670,6 +17210,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +flatstr@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.12.tgz#c2ba6a08173edbb6c9640e3055b95e287ceb5931" + integrity sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw== + flatted@^3.1.0, flatted@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" @@ -16808,7 +17353,7 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^2.1.2: +formidable@^2.0.1, formidable@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== @@ -16870,7 +17415,7 @@ from@^0.1.7, from@~0: resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== -fromentries@^1.3.2: +fromentries@^1.3.1, fromentries@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== @@ -16908,7 +17453,7 @@ fs-extra@9.1.0, fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^11.0.0: +fs-extra@^11.0.0, fs-extra@^11.1.0: version "11.1.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== @@ -17581,7 +18126,7 @@ glob@^5.0.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.1, glob@^8.0.3: +glob@^8.0.0, glob@^8.0.1, glob@^8.0.3, glob@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== @@ -18230,6 +18775,14 @@ help-me@^3.0.0: glob "^7.1.6" readable-stream "^3.6.0" +help-me@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563" + integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA== + dependencies: + glob "^8.0.0" + readable-stream "^3.6.0" + hexoid@1.0.0, hexoid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" @@ -19233,6 +19786,23 @@ ionicons@^4.6.3: resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-4.6.3.tgz#e4f3a3e9b66761eb8c0db27798cc1a8ce97777d2" integrity sha512-cgP+VIr2cTJpMfFyVHTerq6n2jeoiGboVoe3GlaAo5zoSBDAEXORwUZhv6m+lCyxlsHCS3nqPUE+MKyZU71t8Q== +ioredis@^4.27.8: + version "4.28.5" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f" + integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -19304,6 +19874,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-base64@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-base64/-/is-base64-1.1.0.tgz#8ce1d719895030a457c59a7dcaf39b66d99d56b4" + integrity sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -21234,6 +21809,11 @@ jmespath@0.16.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== +jmespath@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w== + joi@^12.0.0: version "12.0.0" resolved "https://registry.yarnpkg.com/joi/-/joi-12.0.0.tgz#46f55e68f4d9628f01bbb695902c8b307ad8d33a" @@ -21243,6 +21823,11 @@ joi@^12.0.0: isemail "3.x.x" topo "2.x.x" +joycon@^3.0.0, joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + jpeg-js@^0.4.1, jpeg-js@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" @@ -21298,7 +21883,7 @@ js-stringify@^1.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.14.1, js-yaml@^3.13.1, js-yaml@^3.9.0: +js-yaml@3.14.1, js-yaml@^3.13.1, js-yaml@^3.14.1, js-yaml@^3.9.0: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -22007,7 +22592,7 @@ less@^4.1.0: needle "^3.1.0" source-map "~0.6.0" -leven@^2.1.0: +leven@2.1.0, leven@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" integrity sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA== @@ -22492,6 +23077,17 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +load-json-file@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3" + integrity sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw== + dependencies: + graceful-fs "^4.1.15" + parse-json "^4.0.0" + pify "^4.0.1" + strip-bom "^3.0.0" + type-fest "^0.3.0" + load-json-file@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" @@ -22745,7 +23341,7 @@ lodash.get@^4.0.0, lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== -lodash.isarguments@^3.0.0: +lodash.isarguments@^3.0.0, lodash.isarguments@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== @@ -23087,6 +23683,11 @@ lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== +lru-cache@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.2.tgz#255fdbc14b75589d6d0e73644ca167a8db506835" + integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -24403,7 +25004,7 @@ moo@^0.5.0, moo@^0.5.1: resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== -morgan@^1.10.0: +morgan@^1.10.0, morgan@^1.9.1: version "1.10.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== @@ -24458,6 +25059,11 @@ mqtt@^4.3.7: ws "^7.5.5" xtend "^4.0.2" +mri@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" + integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== + mri@^1.1.5: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -25188,6 +25794,11 @@ node-mac@^1.0.1: plist "^3.0.1" yargs "^15.4.0" +node-machine-id@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/node-machine-id/-/node-machine-id-1.1.12.tgz#37904eee1e59b320bb9c5d6c0a59f3b469cb6267" + integrity sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ== + node-notifier@^8.0.0: version "8.0.2" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5" @@ -25935,6 +26546,22 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== +nx-cloud@16.3.0: + version "16.3.0" + resolved "https://registry.yarnpkg.com/nx-cloud/-/nx-cloud-16.3.0.tgz#f916c0be1d7eb5d017d542fea349e09893502ee9" + integrity sha512-hmNgpeLO4v4WDSWa8YhwX+q+9ohIyY8iqxlWyIKixWzQH2XfRgYFjOLH4IDLGOlKa3hg7MB6+4+75cK9CfSmKw== + dependencies: + "@nrwl/nx-cloud" "16.3.0" + axios "1.1.3" + chalk "^4.1.0" + dotenv "~10.0.0" + fs-extra "^11.1.0" + node-machine-id "^1.1.12" + open "~8.4.0" + strip-json-comments "^3.1.1" + tar "6.1.11" + yargs-parser ">=21.1.1" + nx@13.10.6: version "13.10.6" resolved "https://registry.yarnpkg.com/nx/-/nx-13.10.6.tgz#ad9f0afcd3cbc8a6974a477a2c58213823c0b9d2" @@ -26120,11 +26747,41 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== +octokit-auth-probot@^1.2.2: + version "1.2.9" + resolved "https://registry.yarnpkg.com/octokit-auth-probot/-/octokit-auth-probot-1.2.9.tgz#835178f2f13209687c23e794af84b373f234b01a" + integrity sha512-mMjw6Y760EwJnW2tSVooJK8BMdsG6D40SoCclnefVf/5yWjaNVquEu8NREBVWb60OwbpnMEz4vREXHB5xdMFYQ== + dependencies: + "@octokit/auth-app" "^4.0.2" + "@octokit/auth-token" "^3.0.0" + "@octokit/auth-unauthenticated" "^3.0.0" + "@octokit/types" "^8.0.0" + +octokit@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/octokit/-/octokit-2.1.0.tgz#93863ce6630d358327d3959ca5d08a97fd3606b2" + integrity sha512-Pxi6uKTjBRZWgAwsw1NgHdRlL+QASCN35OYS7X79o7PtBME0CLXEroZmPtEwlWZbPTP+iDbEy2wCbSOgm0uGIQ== + dependencies: + "@octokit/app" "^13.1.5" + "@octokit/core" "^4.2.1" + "@octokit/oauth-app" "^4.2.1" + "@octokit/plugin-paginate-rest" "^6.1.0" + "@octokit/plugin-rest-endpoint-methods" "^7.1.1" + "@octokit/plugin-retry" "^4.1.3" + "@octokit/plugin-throttling" "^5.2.2" + "@octokit/request-error" "^v3.0.3" + "@octokit/types" "^9.2.2" + omggif@^1.0.10, omggif@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw== +on-exit-leak-free@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" + integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -26182,7 +26839,7 @@ open@^6.4.0: dependencies: is-wsl "^1.1.0" -open@^8.0.9, open@^8.4.0: +open@^8.0.9, open@^8.4.0, open@~8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== @@ -26447,7 +27104,7 @@ p-map-series@^2.1.0: resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== -p-map@^2.0.0: +p-map@^2.0.0, p-map@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== @@ -27444,6 +28101,89 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== +pino-abstract-transport@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-http@^5.3.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-5.8.0.tgz#6e688fd5f965c5b6991f340eb660ea2927be9aa7" + integrity sha512-YwXiyRb9y0WCD1P9PcxuJuh3Dc5qmXde/paJE86UGYRdiFOi828hR9iUGmk5gaw6NBT9gLtKANOHFimvh19U5w== + dependencies: + fast-url-parser "^1.1.3" + pino "^6.13.0" + pino-std-serializers "^4.0.0" + +pino-pretty@*: + version "10.2.0" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-10.2.0.tgz#c674a153e15c08d7032a826d0051d786feace1d9" + integrity sha512-tRvpyEmGtc2D+Lr3FulIZ+R1baggQ4S3xD2Ar93KixFEDx6SEAUP3W5aYuEw1C73d6ROrNcB2IXLteW8itlwhA== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^3.0.0" + fast-safe-stringify "^2.1.1" + help-me "^4.0.1" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.0.0" + pump "^3.0.0" + readable-stream "^4.0.0" + secure-json-parse "^2.4.0" + sonic-boom "^3.0.0" + strip-json-comments "^3.1.1" + +pino-pretty@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-6.0.0.tgz#4d7ac8528ad74d90040082816202bb7d48eb1c95" + integrity sha512-jyeR2fXXWc68st1DTTM5NhkHlx8p+1fKZMfm84Jwq+jSw08IwAjNaZBZR6ts69hhPOfOjg/NiE1HYW7vBRPL3A== + dependencies: + "@hapi/bourne" "^2.0.0" + args "^5.0.1" + colorette "^1.3.0" + dateformat "^4.5.1" + fast-safe-stringify "^2.0.7" + jmespath "^0.15.0" + joycon "^3.0.0" + pump "^3.0.0" + readable-stream "^3.6.0" + rfdc "^1.3.0" + split2 "^3.1.1" + strip-json-comments "^3.1.1" + +pino-std-serializers@*, pino-std-serializers@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" + integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== + +pino-std-serializers@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz#b56487c402d882eb96cd67c257868016b61ad671" + integrity sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg== + +pino-std-serializers@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1791ccd2539c091ae49ce9993205e2cd5dbba1e2" + integrity sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q== + +pino@^6.13.0, pino@^6.7.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-6.14.0.tgz#b745ea87a99a6c4c9b374e4f29ca7910d4c69f78" + integrity sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg== + dependencies: + fast-redact "^3.0.0" + fast-safe-stringify "^2.0.8" + flatstr "^1.0.12" + pino-std-serializers "^3.1.0" + process-warning "^1.0.0" + quick-format-unescaped "^4.0.3" + sonic-boom "^1.0.2" + pirates@^4.0.1, pirates@^4.0.4, pirates@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -27475,6 +28215,14 @@ pkg-conf@^2.1.0: find-up "^2.0.0" load-json-file "^4.0.0" +pkg-conf@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-3.1.0.tgz#d9f9c75ea1bae0e77938cde045b276dac7cc69ae" + integrity sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ== + dependencies: + find-up "^3.0.0" + load-json-file "^5.2.0" + "pkg-dir@< 6 >= 5": version "5.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" @@ -28455,6 +29203,45 @@ preview-email@^3.0.5: pug "^3.0.2" uuid "^9.0.0" +probot@^12.3.1: + version "12.3.1" + resolved "https://registry.yarnpkg.com/probot/-/probot-12.3.1.tgz#6a19f3faf941978df04afb2dd3f5f65422450c03" + integrity sha512-ECSgycmAC0ILEK6cOa+x3QPufP5JybsuohOFCYr3glQU5SkbmypZJE/Sfio9mxAFHK5LCXveIDsfZCxf6ck4JA== + dependencies: + "@octokit/core" "^3.2.4" + "@octokit/plugin-enterprise-compatibility" "^1.2.8" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-rest-endpoint-methods" "^5.0.1" + "@octokit/plugin-retry" "^3.0.6" + "@octokit/plugin-throttling" "^3.3.4" + "@octokit/types" "^8.0.0" + "@octokit/webhooks" "^9.8.4" + "@probot/get-private-key" "^1.1.0" + "@probot/octokit-plugin-config" "^1.0.0" + "@probot/pino" "^2.2.0" + "@types/express" "^4.17.9" + "@types/ioredis" "^4.27.1" + "@types/pino" "^6.3.4" + "@types/pino-http" "^5.0.6" + commander "^6.2.0" + deepmerge "^4.2.2" + deprecation "^2.3.1" + dotenv "^8.2.0" + eventsource "^2.0.2" + express "^4.17.1" + express-handlebars "^6.0.3" + ioredis "^4.27.8" + js-yaml "^3.14.1" + lru-cache "^6.0.0" + octokit-auth-probot "^1.2.2" + pino "^6.7.0" + pino-http "^5.3.0" + pkg-conf "^3.1.0" + resolve "^1.19.0" + semver "^7.3.4" + update-dotenv "^1.1.1" + uuid "^8.3.2" + proc-log@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77" @@ -28958,6 +29745,11 @@ queue@6.0.2: dependencies: inherits "~2.0.3" +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -29416,6 +30208,17 @@ readable-stream@2.3.7: isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@^4.0.0: + version "4.4.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.4.2.tgz#e6aced27ad3b9d726d8308515b9a1b98dc1b9d13" + integrity sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-web-to-node-stream@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" @@ -29504,7 +30307,7 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -redis-commands@^1.7.0: +redis-commands@1.7.0, redis-commands@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== @@ -30362,6 +31165,11 @@ secure-compare@3.0.1: resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw== +secure-json-parse@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + seed-random@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" @@ -30896,6 +31704,17 @@ smart-buffer@^4.0.2, smart-buffer@^4.1.0, smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +smee-client@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/smee-client/-/smee-client-1.2.3.tgz#a0ef5e86e3640870f19f3953aaf228fa478b082c" + integrity sha512-uDrU8u9/Ln7aRXyzGHgVaNUS8onHZZeSwQjCdkMoSL7U85xI+l+Y2NgjibkMJAyXkW7IAbb8rw9RMHIjS6lAwA== + dependencies: + commander "^2.19.0" + eventsource "^1.1.0" + morgan "^1.9.1" + superagent "^7.1.3" + validator "^13.7.0" + smooth-scrollbar@^8.7.4: version "8.8.1" resolved "https://registry.yarnpkg.com/smooth-scrollbar/-/smooth-scrollbar-8.8.1.tgz#7e0274fbc2a427cbe816c89875c72cac592fbb2f" @@ -31087,6 +31906,28 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" +sonic-boom@^1.0.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" + integrity sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg== + dependencies: + atomic-sleep "^1.0.0" + flatstr "^1.0.12" + +sonic-boom@^2.1.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611" + integrity sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg== + dependencies: + atomic-sleep "^1.0.0" + +sonic-boom@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c" + integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g== + dependencies: + atomic-sleep "^1.0.0" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -31311,13 +32152,18 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split2@^3.0.0, split2@^3.1.0: +split2@^3.0.0, split2@^3.1.0, split2@^3.1.1: version "3.2.2" resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== dependencies: readable-stream "^3.0.0" +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + split2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" @@ -31497,6 +32343,11 @@ stacktrace-js@^2.0.0: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + stat-mode@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" @@ -31924,7 +32775,7 @@ strip-indent@^4.0.0: dependencies: min-indent "^1.0.1" -strip-json-comments@3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -32092,6 +32943,23 @@ sumchecker@^3.0.1: dependencies: debug "^4.1.0" +superagent@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-7.1.6.tgz#64f303ed4e4aba1e9da319f134107a54cacdc9c6" + integrity sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" + superagent@^8.0.5: version "8.0.9" resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" @@ -32277,6 +33145,18 @@ tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" +tar@6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + tar@^4.4.10, tar@^4.4.12, tar@^4.4.19, tar@^4.4.8: version "4.4.19" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" @@ -33011,7 +33891,7 @@ tslib@2.5.0, tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.2.0, t resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tslib@^1.10.0, tslib@^1.13.0, tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.10.0, tslib@^1.13.0, tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -33197,6 +34077,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" + integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== + type-fest@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" @@ -33576,6 +34461,14 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +universal-github-app-jwt@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/universal-github-app-jwt/-/universal-github-app-jwt-1.1.1.tgz#d57cee49020662a95ca750a057e758a1a7190e6e" + integrity sha512-G33RTLrIBMFmlDV4u4CBF7dh71eWwykck4XgaxaIVeZKOYZRAAxvcGMRFTUclVY6xoUPQvO4Ne5wKGxYm/Yy9w== + dependencies: + "@types/jsonwebtoken" "^9.0.0" + jsonwebtoken "^9.0.0" + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" @@ -33712,6 +34605,11 @@ update-browserslist-db@^1.0.10: escalade "^3.1.1" picocolors "^1.0.0" +update-dotenv@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-dotenv/-/update-dotenv-1.1.1.tgz#17146f302f216c3c92419d5a327a45be910050ca" + integrity sha512-3cIC18In/t0X/yH793c00qqxcKD8jVCgNOPif/fGQkFpYMGecM9YAc+kaAKXuZsM2dE9I9wFI7KvAuNX22SGMQ== + update-notifier@^2.3.0, update-notifier@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" @@ -35069,7 +35967,7 @@ yargs-parser@21.0.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== -yargs-parser@21.1.1, yargs-parser@^21.1.1: +yargs-parser@21.1.1, yargs-parser@>=21.1.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==