diff --git a/.cspell.json b/.cspell.json index b2b17509e3a..ed2b01750cb 100644 --- a/.cspell.json +++ b/.cspell.json @@ -453,7 +453,28 @@ "TIMESTAMPDIFF", "listbox", "combobox", - "tabid" + "tabid", + "fesm", + "Seedable", + "npmignore", + "undici", + "libsql", + "knexjs", + "zipkin", + "honeycombio", + "gauzyapiserver", + "Embeddable", + "locutus", + "woot", + "proto", + "iubenda", + "iconed", + "cust", + "trasp", + "policyicon", + "wbars", + "allcaps", + "iubgreen" ], "useGitignore": true, "ignorePaths": [ diff --git a/angular.json b/angular.json index f42971d94f7..6e1d6383a9e 100755 --- a/angular.json +++ b/angular.json @@ -73,7 +73,6 @@ "allowedCommonJsDependencies": [ "@gauzy/contracts", "@gauzy/ui-config", - "@gauzy/integration-hubstaff", "brace", "brace/mode/handlebars", "camelcase", @@ -85,7 +84,8 @@ "moment-timezone", "randomcolor", "underscore.string", - "slugify" + "slugify", + "eva-icons" ] }, "configurations": { @@ -288,7 +288,7 @@ } } }, - "job-search-ui-plugin": { + "plugin-job-search-ui": { "projectType": "library", "root": "packages/plugins/job-search-ui", "sourceRoot": "packages/plugins/job-search-ui", diff --git a/apps/api/package.json b/apps/api/package.json index 86530f45f79..c1130376bc8 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -40,14 +40,16 @@ "seed:prod:build": "yarn ng run api:seed -c=production" }, "dependencies": { - "@gauzy/changelog-plugin": "^0.1.0", "@gauzy/core": "^0.1.0", - "@gauzy/integration-upwork": "^0.1.0", - "@gauzy/jitsu-analytics-plugin": "^0.1.0", - "@gauzy/job-proposal-plugin": "^0.1.0", - "@gauzy/job-search-plugin": "^0.1.0", - "@gauzy/knowledge-base-plugin": "^0.1.0", - "@gauzy/sentry-plugin": "^0.1.0", + "@gauzy/plugin-changelog": "^0.1.0", + "@gauzy/plugin-integration-github": "^0.1.0", + "@gauzy/plugin-integration-hubstaff": "^0.1.0", + "@gauzy/plugin-integration-upwork": "^0.1.0", + "@gauzy/plugin-jitsu-analytics": "^0.1.0", + "@gauzy/plugin-job-proposal": "^0.1.0", + "@gauzy/plugin-job-search": "^0.1.0", + "@gauzy/plugin-knowledge-base": "^0.1.0", + "@gauzy/plugin-sentry": "^0.1.0", "dotenv": "^16.0.3", "yargs": "^17.5.0" }, diff --git a/apps/api/src/plugin-config.ts b/apps/api/src/plugin-config.ts index c2587d43e7d..5ad3eb019d4 100644 --- a/apps/api/src/plugin-config.ts +++ b/apps/api/src/plugin-config.ts @@ -13,7 +13,7 @@ import { environment, dbKnexConnectionConfig } from '@gauzy/config'; -import { SentryService } from '@gauzy/sentry-plugin'; +import { SentryService } from '@gauzy/plugin-sentry'; import { SentryTracing as SentryPlugin } from './sentry'; import { version } from './../version'; import { plugins } from './plugins'; diff --git a/apps/api/src/plugins.ts b/apps/api/src/plugins.ts index 9fc99263c75..d0c47fb562b 100644 --- a/apps/api/src/plugins.ts +++ b/apps/api/src/plugins.ts @@ -1,10 +1,12 @@ import { environment } from '@gauzy/config'; -import { ChangelogPlugin } from '@gauzy/changelog-plugin'; -import { JitsuAnalyticsPlugin } from '@gauzy/jitsu-analytics-plugin'; -import { KnowledgeBasePlugin } from '@gauzy/knowledge-base-plugin'; -import { JobProposalPlugin } from '@gauzy/job-proposal-plugin'; -import { JobSearchPlugin } from '@gauzy/job-search-plugin'; -import { IntegrationUpworkPlugin } from '@gauzy/integration-upwork'; +import { ChangelogPlugin } from '@gauzy/plugin-changelog'; +import { IntegrationGithubPlugin } from '@gauzy/plugin-integration-github'; +import { IntegrationHubstaffPlugin } from '@gauzy/plugin-integration-hubstaff'; +import { IntegrationUpworkPlugin } from '@gauzy/plugin-integration-upwork'; +import { JitsuAnalyticsPlugin } from '@gauzy/plugin-jitsu-analytics'; +import { JobProposalPlugin } from '@gauzy/plugin-job-proposal'; +import { JobSearchPlugin } from '@gauzy/plugin-job-search'; +import { KnowledgeBasePlugin } from '@gauzy/plugin-knowledge-base'; import { SentryTracing as SentryPlugin } from './sentry'; const { jitsu, sentry } = environment; @@ -26,12 +28,16 @@ export const plugins = [ }), // Indicates the inclusion or intention to use the ChangelogPlugin in the codebase. ChangelogPlugin, - // Indicates the inclusion or intention to use the KnowledgeBasePlugin in the codebase. - KnowledgeBasePlugin, + // Indicates the inclusion or intention to use the IntegrationGithubPlugin in the codebase. + IntegrationGithubPlugin, + // Indicates the inclusion or intention to use the IntegrationHubstaffPlugin in the codebase. + IntegrationHubstaffPlugin, + // Indicates the inclusion or intention to use the IntegrationUpworkPlugin in the codebase. + IntegrationUpworkPlugin, // Indicates the inclusion or intention to use the JobProposalPlugin in the codebase. JobProposalPlugin, // Indicates the inclusion or intention to use the JobSearchPlugin in the codebase. JobSearchPlugin, - // Indicates the inclusion or intention to use the IntegrationUpworkPlugin in the codebase. - IntegrationUpworkPlugin + // Indicates the inclusion or intention to use the KnowledgeBasePlugin in the codebase. + KnowledgeBasePlugin ]; diff --git a/apps/api/src/sentry.ts b/apps/api/src/sentry.ts index 221e0b29d35..761e12aea86 100644 --- a/apps/api/src/sentry.ts +++ b/apps/api/src/sentry.ts @@ -1,5 +1,5 @@ import { environment } from '@gauzy/config'; -import { SentryPlugin, DefaultSentryIntegrations } from '@gauzy/sentry-plugin'; +import { SentryPlugin, DefaultSentryIntegrations } from '@gauzy/plugin-sentry'; import { version } from '../version'; /** diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 919acb540fd..71a7e557d28 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -1,196 +1,198 @@ { - "name": "gauzy-desktop", - "productName": "Gauzy Desktop", - "version": "0.1.0", - "description": "Gauzy Desktop", - "license": "AGPL-3.0", - "homepage": "https://gauzy.co", - "repository": { - "type": "git", - "url": "https://github.com/ever-co/ever-gauzy.git" - }, - "bugs": { - "url": "https://github.com/ever-co/ever-gauzy/issues" - }, - "private": true, - "author": { - "name": "Ever Co. LTD", - "email": "ever@ever.co", - "url": "https://ever.co" - }, - "main": "index.js", - "workspaces": { - "packages": [ - "../../../packages/core", - "../../../packages/auth", - "../../../packages/desktop-window", - "../../../packages/desktop-libs", - "../../../packages/common", - "../../../packages/config", - "../../../packages/contracts", - "../../../packages/ui-config", - "../../../packages/plugins/integration-ai", - "../../../packages/plugins/integration-hubstaff", - "../../../packages/plugins/integration-upwork", - "../../../packages/plugins/integration-github", - "../../../packages/plugins/integration-jira", - "../../../packages/plugin", - "../../../packages/plugins/knowledge-base", - "../../../packages/plugins/changelog", - "../../../packages/plugins/jitsu-analytics", - "../../../packages/plugins/sentry-tracing", - "../../../packages/plugins/job-search", - "../../../packages/plugins/job-proposal" - ] - }, - "build": { - "appId": "com.ever.gauzydesktop", - "artifactName": "${name}-${version}.${ext}", - "productName": "Gauzy Desktop", - "copyright": "Copyright © 2019-Present. Ever Co. LTD", - "dmg": { - "sign": false - }, - "asar": true, - "npmRebuild": true, - "asarUnpack": [ - "node_modules/screenshot-desktop/lib/win32", - "node_modules/@sentry/electron", - "node_modules/sqlite3/lib", - "node_modules/better-sqlite3", - "node_modules/@sentry/profiling-node/lib" - ], - "directories": { - "buildResources": "icons", - "output": "../desktop-packages" - }, - "publish": [ - { - "provider": "github", - "repo": "ever-gauzy-desktop", - "releaseType": "release" - }, - { - "provider": "spaces", - "name": "ever", - "region": "sfo3", - "path": "/ever-gauzy-desktop", - "acl": "public-read" - } - ], - "mac": { - "category": "public.app-category.developer-tools", - "icon": "icon.icns", - "target": [ - "zip", - "dmg" - ], - "asarUnpack": "**/*.node", - "artifactName": "${name}-${version}.${ext}", - "hardenedRuntime": true, - "gatekeeperAssess": false, - "entitlements": "tools/build/entitlements.mas.plist", - "entitlementsInherit": "tools/build/entitlements.mas.plist", - "extendInfo": { - "NSAppleEventsUsageDescription": "Please allow access to script browser applications to detect the current URL when triggering instant lookup." - } - }, - "win": { - "publisherName": "Ever", - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } - ], - "icon": "icon.ico", - "verifyUpdateCodeSignature": false - }, - "linux": { - "icon": "linux", - "target": [ - "AppImage", - "deb", - "tar.gz" - ], - "executableName": "gauzy-desktop", - "artifactName": "${name}-${version}.${ext}", - "synopsis": "Desktop", - "category": "Development" - }, - "nsis": { - "oneClick": false, - "perMachine": true, - "createDesktopShortcut": true, - "createStartMenuShortcut": true, - "allowToChangeInstallationDirectory": true, - "allowElevation": true, - "installerIcon": "icon.ico", - "artifactName": "${name}-${version}.${ext}", - "deleteAppDataOnUninstall": true, - "menuCategory": true - }, - "extraResources": [ - "./data/**/*" - ] - }, - "dependencies": { - "@datorama/akita-ngdevtools": "^7.0.0", - "@datorama/akita": "^7.1.1", - "@electron/remote": "^2.0.8", - "@gauzy/auth": "^0.1.0", - "@gauzy/changelog-plugin": "^0.1.0", - "@gauzy/core": "^0.1.0", - "@gauzy/desktop-libs": "^0.1.0", - "@gauzy/desktop-window": "^0.1.0", - "@gauzy/jitsu-analytics-plugin": "^0.1.0", - "@gauzy/job-proposal-plugin": "^0.1.0", - "@gauzy/job-search-plugin": "^0.1.0", - "@gauzy/knowledge-base-plugin": "^0.1.0", - "@gauzy/sentry-plugin": "^0.1.0", - "@nestjs/platform-express": "^10.3.7", - "@sentry/electron": "^4.18.0", - "@sentry/node": "^7.101.1", - "@sentry/profiling-node": "^7.101.1", - "@sentry/replay": "^7.101.1", - "@sentry/tracing": "^7.101.1", - "@sentry/types": "^7.101.1", - "auto-launch": "5.0.5", - "consolidate": "^0.16.0", - "electron-log": "^4.4.8", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.7", - "electron-util": "^0.17.2", - "embedded-queue": "^0.0.11", - "ffi-napi": "^4.0.3", - "form-data": "^3.0.0", - "htmlparser2": "^8.0.2", - "iconv": "^3.0.1", - "knex": "^3.1.0", - "libsql": "^0.3.16", - "locutus": "^2.0.30", - "mac-screen-capture-permissions": "^2.1.0", - "moment": "^2.30.1", - "node-fetch": "^2.6.7", - "node-notifier": "^8.0.0", - "node-static": "^0.7.11", - "pdfmake": "^0.2.0", - "pg-query-stream": "^4.5.4", - "pg": "^8.11.4", - "screenshot-desktop": "^1.15.0", - "sound-play": "1.1.0", - "sqlite3": "^5.1.7", - "squirrelly": "^8.0.8", - "tslib": "^2.6.2", - "twing": "^5.0.2", - "underscore": "^1.13.3", - "undici": "^6.10.2" - }, - "optionalDependencies": { - "node-linux": "^0.1.12", - "node-mac": "^1.0.1", - "node-windows": "^1.0.0-beta.8" - } -} + "name": "gauzy-desktop", + "productName": "Ever Gauzy Desktop", + "version": "0.1.0", + "description": "Ever Gauzy Desktop", + "license": "AGPL-3.0", + "homepage": "https://gauzy.co", + "repository": { + "type": "git", + "url": "https://github.com/ever-co/ever-gauzy.git" + }, + "bugs": { + "url": "https://github.com/ever-co/ever-gauzy/issues" + }, + "private": true, + "author": { + "name": "Ever Co. LTD", + "email": "ever@ever.co", + "url": "https://ever.co" + }, + "main": "index.js", + "workspaces": { + "packages": [ + "../../../packages/core", + "../../../packages/auth", + "../../../packages/desktop-window", + "../../../packages/desktop-libs", + "../../../packages/common", + "../../../packages/config", + "../../../packages/contracts", + "../../../packages/ui-config", + "../../../packages/plugins/integration-ai", + "../../../packages/plugins/integration-hubstaff", + "../../../packages/plugins/integration-upwork", + "../../../packages/plugins/integration-github", + "../../../packages/plugins/integration-jira", + "../../../packages/plugin", + "../../../packages/plugins/knowledge-base", + "../../../packages/plugins/changelog", + "../../../packages/plugins/jitsu-analytics", + "../../../packages/plugins/sentry-tracing", + "../../../packages/plugins/job-search", + "../../../packages/plugins/job-proposal" + ] + }, + "build": { + "appId": "com.ever.gauzydesktop", + "artifactName": "${name}-${version}.${ext}", + "productName": "Ever Gauzy Desktop", + "copyright": "Copyright © 2019-Present. Ever Co. LTD", + "dmg": { + "sign": false + }, + "asar": true, + "npmRebuild": true, + "asarUnpack": [ + "node_modules/screenshot-desktop/lib/win32", + "node_modules/@sentry/electron", + "node_modules/sqlite3/lib", + "node_modules/better-sqlite3", + "node_modules/@sentry/profiling-node/lib" + ], + "directories": { + "buildResources": "icons", + "output": "../desktop-packages" + }, + "publish": [ + { + "provider": "github", + "repo": "ever-gauzy-desktop", + "releaseType": "release" + }, + { + "provider": "spaces", + "name": "ever", + "region": "sfo3", + "path": "/ever-gauzy-desktop", + "acl": "public-read" + } + ], + "mac": { + "category": "public.app-category.developer-tools", + "icon": "icon.icns", + "target": [ + "zip", + "dmg" + ], + "asarUnpack": "**/*.node", + "artifactName": "${name}-${version}.${ext}", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "tools/build/entitlements.mas.plist", + "entitlementsInherit": "tools/build/entitlements.mas.plist", + "extendInfo": { + "NSAppleEventsUsageDescription": "Please allow access to script browser applications to detect the current URL when triggering instant lookup." + } + }, + "win": { + "publisherName": "Ever", + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ], + "icon": "icon.ico", + "verifyUpdateCodeSignature": false + }, + "linux": { + "icon": "linux", + "target": [ + "AppImage", + "deb", + "tar.gz" + ], + "executableName": "gauzy-desktop", + "artifactName": "${name}-${version}.${ext}", + "synopsis": "Desktop", + "category": "Development" + }, + "nsis": { + "oneClick": false, + "perMachine": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "allowToChangeInstallationDirectory": true, + "allowElevation": true, + "installerIcon": "icon.ico", + "artifactName": "${name}-${version}.${ext}", + "deleteAppDataOnUninstall": true, + "menuCategory": true + }, + "extraResources": [ + "./data/**/*" + ] + }, + "dependencies": { + "@datorama/akita-ngdevtools": "^7.0.0", + "@datorama/akita": "^7.1.1", + "@electron/remote": "^2.0.8", + "@gauzy/auth": "^0.1.0", + "@gauzy/core": "^0.1.0", + "@gauzy/desktop-libs": "^0.1.0", + "@gauzy/desktop-window": "^0.1.0", + "@gauzy/plugin-integration-hubstaff": "^0.1.0", + "@gauzy/plugin-changelog": "^0.1.0", + "@gauzy/plugin-integration-upwork": "^0.1.0", + "@gauzy/plugin-jitsu-analytics": "^0.1.0", + "@gauzy/plugin-job-proposal": "^0.1.0", + "@gauzy/plugin-job-search": "^0.1.0", + "@gauzy/plugin-knowledge-base": "^0.1.0", + "@gauzy/plugin-sentry": "^0.1.0", + "@nestjs/platform-express": "^10.3.7", + "@sentry/electron": "^4.18.0", + "@sentry/node": "^7.101.1", + "@sentry/profiling-node": "^7.101.1", + "@sentry/replay": "^7.101.1", + "@sentry/tracing": "^7.101.1", + "@sentry/types": "^7.101.1", + "auto-launch": "5.0.5", + "consolidate": "^0.16.0", + "electron-log": "^4.4.8", + "electron-store": "^8.1.0", + "electron-updater": "^6.1.7", + "electron-util": "^0.17.2", + "embedded-queue": "^0.0.11", + "ffi-napi": "^4.0.3", + "form-data": "^3.0.0", + "htmlparser2": "^8.0.2", + "iconv": "^3.0.1", + "knex": "^3.1.0", + "libsql": "^0.3.16", + "locutus": "^2.0.30", + "mac-screen-capture-permissions": "^2.1.0", + "moment": "^2.30.1", + "node-fetch": "^2.6.7", + "node-notifier": "^8.0.0", + "node-static": "^0.7.11", + "pdfmake": "^0.2.0", + "pg-query-stream": "^4.5.4", + "pg": "^8.11.4", + "screenshot-desktop": "^1.15.0", + "sound-play": "1.1.0", + "sqlite3": "^5.1.7", + "squirrelly": "^8.0.8", + "tslib": "^2.6.2", + "twing": "^5.0.2", + "underscore": "^1.13.3", + "undici": "^6.10.2" + }, + "optionalDependencies": { + "node-linux": "^0.1.12", + "node-mac": "^1.0.1", + "node-windows": "^1.0.0-beta.8" + } +} \ No newline at end of file diff --git a/apps/gauzy/package.json b/apps/gauzy/package.json index 3c38fa47175..5f161a250da 100644 --- a/apps/gauzy/package.json +++ b/apps/gauzy/package.json @@ -90,8 +90,8 @@ "camelcase": "^6.3.0", "chart.js": "^4.4.1", "chart.piecelabel.js": "^0.15.0", - "ckeditor4": "^4.23.0", - "ckeditor4-angular": "^5.1.0", + "ckeditor4": "4.22.1", + "ckeditor4-angular": "4.0.1", "core-js": "^3.8.3", "d3": "^7.4.4", "d3-selection-multi": "^1.0.1", diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-main/edit-employee-main.component.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-main/edit-employee-main.component.ts index 8540aeb73d5..fd4a63ea18f 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-main/edit-employee-main.component.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-main/edit-employee-main.component.ts @@ -5,7 +5,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { combineLatest } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; import { Store } from '@gauzy/ui-core/common'; -import { EmployeeStore, ToastrService } from '@gauzy/ui-core/core'; +import { EmployeeStore, ErrorHandlingService } from '@gauzy/ui-core/core'; /** * This component contains the properties stored within the User Entity of an Employee. @@ -28,7 +28,7 @@ export class EditEmployeeMainComponent implements OnInit, OnDestroy { /* * Employee Main Mutation Form */ - public form: UntypedFormGroup = EditEmployeeMainComponent.buildForm(this.fb); + public form: UntypedFormGroup = EditEmployeeMainComponent.buildForm(this._fb); static buildForm(fb: UntypedFormBuilder): UntypedFormGroup { return fb.group({ username: [], @@ -43,15 +43,15 @@ export class EditEmployeeMainComponent implements OnInit, OnDestroy { } constructor( - private readonly fb: UntypedFormBuilder, - private readonly store: Store, - private readonly toastrService: ToastrService, - private readonly employeeStore: EmployeeStore + private readonly _fb: UntypedFormBuilder, + private readonly _store: Store, + private readonly _employeeStore: EmployeeStore, + private readonly _errorHandlingService: ErrorHandlingService ) {} ngOnInit() { - const storeOrganization$ = this.store.selectedOrganization$; - const storeEmployee$ = this.employeeStore.selectedEmployee$; + const storeOrganization$ = this._store.selectedOrganization$; + const storeEmployee$ = this._employeeStore.selectedEmployee$; combineLatest([storeOrganization$, storeEmployee$]) .pipe( filter(([organization, employee]) => !!organization && !!employee), @@ -65,8 +65,14 @@ export class EditEmployeeMainComponent implements OnInit, OnDestroy { .subscribe(); } + /** + * Handles errors that occur during image upload. + * + * @param error - The error object to handle. + */ handleImageUploadError(error: any) { - this.toastrService.danger(error); + // Delegate error handling to the _errorHandlingService + this._errorHandlingService.handleError(error); } /** @@ -74,15 +80,20 @@ export class EditEmployeeMainComponent implements OnInit, OnDestroy { * * @param image */ - updateImageAsset(image: IImageAsset) { + async updateImageAsset(image: IImageAsset) { try { if (image) { - this.employeeStore.userForm = { - imageId: image.id - }; + console.log('image', image); + + // Update user form data in store (assuming updateUserForm is async) + await this._employeeStore.updateUserForm({ + imageId: image.id, + image + }); } } catch (error) { - this.handleImageUploadError(error); + // Handle and log errors + this._errorHandlingService.handleError(error); } } @@ -91,12 +102,11 @@ export class EditEmployeeMainComponent implements OnInit, OnDestroy { * * @returns */ - submitForm() { + async submitForm() { if (this.form.invalid || !this.organization) { return; } - const { id: organizationId } = this.organization; - const { tenantId } = this.store.user; + const { id: organizationId, tenantId } = this.organization; const values = { organizationId, @@ -104,17 +114,24 @@ export class EditEmployeeMainComponent implements OnInit, OnDestroy { ...(this.form.valid ? this.form.value : {}) }; - this.employeeStore.userForm = values; - this.employeeStore.employeeForm = values; + // Update user form data in store (assuming updateUserForm is async) + await this._employeeStore.updateUserForm(values); + await this._employeeStore.updateEmployeeForm(values); } + /** + * Initialize the form values with the given employee's data. + * + * @param employee - The employee whose data will be used to initialize the form. + */ private _initializeFormValue(employee: IEmployee) { + // Patch the form with the employee's user data this.form.patchValue({ username: employee.user.username, email: employee.user.email, firstName: employee.user.firstName, lastName: employee.user.lastName, - imageUrl: employee.user.imageUrl, + imageUrl: employee.user.image?.fullUrl || employee.user.imageUrl, imageId: employee.user.imageId, preferredLanguage: employee.user.preferredLanguage, profile_link: employee.profile_link diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts index 9223f6bdd32..ab700e9fb94 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts @@ -1,14 +1,20 @@ import { Component, OnDestroy, OnInit, Output, EventEmitter } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; -import { IEmployee, IEmployeeUpdateInput, IUserUpdateInput, PermissionsEnum } from '@gauzy/contracts'; import { TranslateService } from '@ngx-translate/core'; import { firstValueFrom, Subject } from 'rxjs'; import { debounceTime, filter, tap } from 'rxjs/operators'; +import { NbRouteTab } from '@nebular/theme'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { IEmployee, IEmployeeUpdateInput, IUserUpdateInput, PermissionsEnum } from '@gauzy/contracts'; import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; import { Store } from '@gauzy/ui-core/common'; -import { ErrorHandlingService, ToastrService, UsersService } from '@gauzy/ui-core/core'; -import { EmployeesService, EmployeeStore } from '@gauzy/ui-core/core'; +import { + EmployeesService, + EmployeeStore, + ErrorHandlingService, + ToastrService, + UsersService +} from '@gauzy/ui-core/core'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -21,7 +27,7 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple routeParams: Params; selectedEmployee: IEmployee; employeeName: string; - tabs: any[] = []; + tabs: NbRouteTab[] = []; subject$: Subject = new Subject(); @Output() @@ -146,6 +152,11 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple ]; } + /** + * Submit the employee form with updated data + * + * @param value - The updated employee form data to submit. + */ private async submitEmployeeForm(value: IEmployeeUpdateInput) { if (value) { try { @@ -154,62 +165,89 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple * But employee can not update whole profile except some of the fields provided by UI * We will define later, which fields allow to employee to update from the form */ - if (!!this.store.hasPermission(PermissionsEnum.ORG_EMPLOYEES_EDIT)) { + if (this.store.hasPermission(PermissionsEnum.ORG_EMPLOYEES_EDIT)) { await this.employeeService.update(this.selectedEmployee.id, value); } else { + // Update only allowed fields if user does not have full edit permission await this.employeeService.updateProfile(this.selectedEmployee.id, value); } - this.toastrService.success('TOASTR.MESSAGE.EMPLOYEE_PROFILE_UPDATE', { - name: this.employeeName - }); + + // Show success message on successful update + this.toastrService.success('TOASTR.MESSAGE.EMPLOYEE_PROFILE_UPDATE', { name: this.employeeName }); } catch (error) { + // Handle and log errors this.errorHandler.handleError(error); } finally { + // Notify subscribers that form submission is complete this.subject$.next(true); } } } /** - * This is to update the User details of an Employee. - * Do NOT use this function to update any details which are NOT stored in the User Entity. + * Submit the user form with updated data + * + * @param user - The updated user data to submit. */ - private async submitUserForm(value: IUserUpdateInput) { - if (value) { + private async submitUserForm(user: IUserUpdateInput) { + if (user) { try { - await this.userService.update(this.selectedEmployee.user.id, value); - this.updatedImage.emit(value.imageUrl); - if (!value.email) { + // Update the user using userService + await this.userService.update(this.selectedEmployee.user.id, user); + + if (!!user.image) { + // Emit event for updated image (assuming this emits an event when the image is updated) + this.updatedImage.emit(user.image); + } + + // Show success message based on whether email was updated or not + if (!user.email) { this.toastrService.success('TOASTR.MESSAGE.IMAGE_UPDATED'); } else { this.toastrService.success('TOASTR.MESSAGE.EMPLOYEE_PROFILE_UPDATE', { name: this.employeeName }); } } catch (error) { + // Handle and log errors this.errorHandler.handleError(error); } finally { + // Notify subscribers that form submission is complete this.subject$.next(true); } } } + /** + * Retrieves and sets the profile of the selected employee + */ private async _getEmployeeProfile() { - const { id } = this.routeParams; - const employee = await firstValueFrom( - this.employeeService.getEmployeeById(id, [ - 'user', - 'organizationDepartments', - 'organizationPosition', - 'organizationEmploymentTypes', - 'tags', - 'skills', - 'contact' - ]) - ); - this.employeeStore.selectedEmployee = this.selectedEmployee = employee; - this.employeeName = employee?.user?.name || employee?.user?.username || 'Employee'; - } + try { + const { id } = this.routeParams; - ngOnDestroy() {} + // Fetch employee data from the service + const employee = await firstValueFrom( + this.employeeService.getEmployeeById(id, [ + 'user', + 'organizationDepartments', + 'organizationPosition', + 'organizationEmploymentTypes', + 'tags', + 'skills', + 'contact' + ]) + ); + + // Set the selected employee in the store and component + this.employeeStore.selectedEmployee = this.selectedEmployee = employee; + + // Set the employee name for display + this.employeeName = employee?.user?.name || employee?.user?.username || 'Employee'; + } catch (error) { + // Handle errors gracefully + console.error('Error fetching employee profile:', error); + // Optionally, navigate to a fallback route or show an error message + // this.router.navigate(['/error']); + } + } private _applyTranslationOnTabs() { this.translateService.onLangChange @@ -219,4 +257,6 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple ) .subscribe(); } + + ngOnDestroy() {} } diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.html b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.html index f77881b76dc..d92bd25fc41 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.html +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.html @@ -1,19 +1,12 @@ - + -
+
Employee Avatar @@ -44,10 +37,7 @@
-
+
{{ 'FORM.USERNAME' | translate }}: {{ selectedEmployee?.user?.username }}
@@ -63,8 +53,6 @@
{{ 'EMPLOYEES_PAGE.SELECT_EMPLOYEE_MSG' | translate }}
- + diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.ts index 2fff28adfe9..6f8084bd1ad 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.component.ts @@ -5,10 +5,11 @@ import { debounceTime } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; -import { IEmployee, IOrganization, ISelectedEmployee, IUser, PermissionsEnum } from '@gauzy/contracts'; +import { IEmployee, IImageAsset, IOrganization, ISelectedEmployee, IUser, PermissionsEnum } from '@gauzy/contracts'; import { Store, distinctUntilChange } from '@gauzy/ui-core/common'; import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; import { ALL_EMPLOYEES_SELECTED } from '@gauzy/ui-core/shared'; +import { ErrorHandlingService } from '@gauzy/ui-core/core'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -28,7 +29,8 @@ export class EditEmployeeComponent extends TranslationBaseComponent implements O public readonly translateService: TranslateService, private readonly cdr: ChangeDetectorRef, private readonly _urlSerializer: UrlSerializer, - private readonly _location: Location + private readonly _location: Location, + private readonly _errorHandlingService: ErrorHandlingService ) { super(translateService); } @@ -92,17 +94,26 @@ export class EditEmployeeComponent extends TranslationBaseComponent implements O .subscribe(); } - updateImage(imageUrl: IUser['imageUrl']) { + /** + * Update the image asset for the selected employee + * @param image The image asset to update + */ + updateImage(image: IImageAsset) { try { - if (imageUrl) { - this.selectedEmployee.user.imageUrl = imageUrl; + if (image) { + // Update the image for the selected employee + this.selectedEmployee.user.image = image; + + // Alternatively, update the selectedEmployee in the store with the new image URL this.store.selectedEmployee = { ...this.store.selectedEmployee, - imageUrl: imageUrl + imageUrl: image?.fullUrl }; } } catch (error) { - console.log('Error while uploading profile avatar', error); + console.error('Error while updating profile avatar:', error); + // Handle and log errors + this._errorHandlingService.handleError(error); } } diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.resolver.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.resolver.ts index 242ef11a350..ecd5dc696b8 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.resolver.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee.resolver.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; -import { catchError, Observable, of as observableOf } from 'rxjs'; +import { catchError, Observable, of } from 'rxjs'; import { IEmployee } from '@gauzy/contracts'; import { EmployeesService } from '@gauzy/ui-core/core'; @@ -10,15 +10,26 @@ import { EmployeesService } from '@gauzy/ui-core/core'; export class EditEmployeeResolver implements Resolve> { constructor(private readonly employeeService: EmployeesService, private readonly router: Router) {} + /** + * Resolve method for fetching employee data by ID. + * + * @param route - The activated route snapshot containing route parameters. + * @returns An observable of type IEmployee, representing the resolved employee data. + */ resolve(route: ActivatedRouteSnapshot): Observable { try { - const employeeId = route.params.id; - return this.employeeService.getEmployeeById(employeeId, ['user', 'organizationPosition']).pipe( + const employeeId = route.params.id; // Extract employee ID from route parameters + const relations = ['user', 'user.image', 'organizationPosition']; // Define relations to include in the query + + // Call the employeeService to fetch employee data by ID with specified relations + return this.employeeService.getEmployeeById(employeeId, relations).pipe( catchError((error) => { - return observableOf(error); + // Handle errors and return as observable of the error + return of(error); }) ); } catch (error) { + // Catch any synchronous errors and navigate to the employee listing page this.router.navigate(['/pages/employees']); } } diff --git a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.html b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.html index 6013b9e3832..6a37d90f04c 100644 --- a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.html +++ b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.html @@ -8,10 +8,7 @@
{{ 'INTEGRATIONS.HUBSTAFF_PAGE.NAME' | translate }}
- +
@@ -19,17 +16,17 @@
{{ 'INTEGRATIONS.HUBSTAFF_PAGE.NAME' | translate }}
@@ -41,13 +38,12 @@
{{ 'INTEGRATIONS.HUBSTAFF_PAGE.NAME' | translate }}
[placeholder]="'INTEGRATIONS.HUBSTAFF_PAGE.SELECT_ORGANIZATION' | translate" (change)="selectOrganization($event)" appendTo="body" - > - + >
diff --git a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.scss b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.scss index ceaa47421ed..4f926ecec1a 100644 --- a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.scss +++ b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.scss @@ -1,23 +1,26 @@ +@import 'gauzy/_gauzy-table.scss'; +@import 'gauzy/_gauzy-cards.scss'; + .hubstaff-container { - min-height: 200px; + min-height: 200px; } .card-header { - display: flex; - justify-content: space-between; - align-items: center; + display: flex; + justify-content: space-between; + align-items: center; } ::ng-deep { - .hubstaff-container { - .angular2-smart-actions { - text-align: center; - width: 5%; + .hubstaff-container { + .angular2-smart-actions { + text-align: center; + width: 5%; - .form-control { - width: 15px; - display: inline-block; - } - } - } + .form-control { + width: 15px; + display: inline-block; + } + } + } } diff --git a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.ts b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.ts index ace252eaedf..930b19b0904 100644 --- a/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.ts +++ b/apps/gauzy/src/app/pages/hubstaff/components/hubstaff/hubstaff.component.ts @@ -3,7 +3,7 @@ import { TitleCasePipe } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; import { ActivatedRoute, Router } from '@angular/router'; import { switchMap, tap, catchError, finalize, map } from 'rxjs/operators'; -import { IHubstaffOrganization, IHubstaffProject, IOrganization } from '@gauzy/contracts'; +import { ID, IHubstaffOrganization, IHubstaffProject, IOrganization } from '@gauzy/contracts'; import { Observable, of, firstValueFrom } from 'rxjs'; import { filter } from 'rxjs/operators'; import { NbDialogService, NbMenuItem, NbMenuService } from '@nebular/theme'; @@ -29,8 +29,8 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni organization: IOrganization; selectedProjects: IHubstaffProject[] = []; loading: boolean; - integrationId: string; - supportContextActions: NbMenuItem[]; + integrationId: ID; + menus: NbMenuItem[] = []; constructor( private readonly _router: Router, @@ -49,7 +49,7 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni ngOnInit() { this._loadSettingsSmartTable(); - this._loadActions(); + this._loadMenus(); this._applyTranslationOnSmartTable(); this._setTokenAndLoadOrganizations(); @@ -75,6 +75,9 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni ngOnDestroy(): void {} + /** + * + */ private _setTokenAndLoadOrganizations() { this.integrationId = this._activatedRoute.snapshot.params.id; this._hubstaffService.getIntegration(this.integrationId).pipe(untilDestroyed(this)).subscribe(); @@ -91,13 +94,9 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni ); } - private _applyTranslationOnSmartTable() { - this.translateService.onLangChange.pipe(untilDestroyed(this)).subscribe(() => { - this._loadSettingsSmartTable(); - this._loadActions(); - }); - } - + /** + * + */ private _loadSettingsSmartTable() { this.settingsSmartTable = { selectedRowIndex: -1, @@ -111,11 +110,13 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni columns: { name: { title: this.getTranslation('SM_TABLE.NAME'), - type: 'string' + type: 'string', + filter: false }, description: { title: this.getTranslation('SM_TABLE.DESCRIPTION'), - type: 'string' + type: 'string', + filter: false }, status: { title: this.getTranslation('SM_TABLE.STATUS'), @@ -126,10 +127,19 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni }; } + /** + * + * @param organization + */ selectOrganization(organization) { this.projects$ = organization ? this._fetchProjects(organization) : of([]); } + /** + * + * @param organization + * @returns + */ private _fetchProjects(organization) { this.loading = true; return this._hubstaffService.getProjects(organization.id, this.integrationId).pipe( @@ -142,10 +152,18 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni ); } + /** + * + * @param param0 + */ selectProject({ selected }) { this.selectedProjects = selected; } + /** + * + * @returns + */ syncProjects() { if (!this.organization) { return; @@ -169,6 +187,10 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni .subscribe(); } + /** + * + * @returns + */ autoSync() { if (!this.organization) { return; @@ -198,6 +220,10 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni .subscribe(); } + /** + * + * @returns + */ async setSettings() { const dialog = this._dialogService.open(SettingsDialogComponent, { context: {} @@ -223,8 +249,11 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni .subscribe(); } - private _loadActions() { - this.supportContextActions = [ + /** + * + */ + private _loadMenus() { + this.menus = [ { title: this.getTranslation('INTEGRATIONS.RE_INTEGRATE'), icon: 'text-outline', @@ -237,6 +266,21 @@ export class HubstaffComponent extends TranslationBaseComponent implements OnIni ]; } + /** + * + */ + private _applyTranslationOnSmartTable() { + this.translateService.onLangChange + .pipe( + tap(() => { + this._loadSettingsSmartTable(); + this._loadMenus(); + }), + untilDestroyed(this) + ) + .subscribe(); + } + /** * Navigate to the "Integrations" page. */ 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 index 38dda8cb99e..43531fa8888 100644 --- 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 @@ -1,9 +1,4 @@ - +
@@ -17,13 +12,7 @@
-
@@ -106,13 +101,13 @@
- {{'INTEGRATIONS.GITHUB_PAGE.AUTO_SYNC_TABLE_LABEL' | translate}} + {{ 'INTEGRATIONS.GITHUB_PAGE.AUTO_SYNC_TABLE_LABEL' | translate }}
@@ -132,7 +127,7 @@
@@ -176,16 +176,14 @@
[settings]="settingsSmartTableIssues" [source]="issues" (userRowSelect)="selectIssues($event)" - style="cursor: pointer;" + style="cursor: pointer" #issuesTable >
- +
- +
@@ -201,8 +199,5 @@
- + 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 index a97e88521e9..4ab3b1763d2 100644 --- 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 @@ -119,7 +119,7 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement if (!projectId) { return EMPTY; // No valid organization, return false } - return this._organizationProjectsService.getById(projectId, ['repository']).pipe( + return this._organizationProjectsService.getById(projectId, ['customFields.repository']).pipe( catchError((error) => { // Handle and log errors this._errorHandlingService.handleError(error); @@ -337,9 +337,7 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement componentInitFunction: (instance: StatusBadgeComponent, cell: Cell) => { instance.value = cell.getValue(); }, - valuePrepareFunction: (value: TaskStatusEnum) => { - return this.getIssueStatus(value); - } + valuePrepareFunction: (value: TaskStatusEnum) => this.getIssueStatus(value) } } }; @@ -362,6 +360,8 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement filter: false, renderComponent: GithubRepositoryComponent, componentInitFunction: (instance: GithubRepositoryComponent, cell: Cell) => { + // Set properties on the ProjectComponent instance + instance.rowData = cell.getRow().getData(); // Set properties on the GithubRepositoryComponent instance instance.value = cell.getRawValue(); } @@ -387,11 +387,9 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement valuePrepareFunction: (_: any, cell: Cell) => { // Get the data of the entire row const row = cell.getRow().getData(); - + const count = row.customFields.repository.issuesCount; // Get repository synced issues count // Prepare the value for the cell by using translation and the 'issuesCount' property from the row - return this.getTranslation('SM_TABLE.ISSUES_SYNC_COUNT', { - count: row?.repository?.issuesCount - }); + return this.getTranslation('SM_TABLE.ISSUES_SYNC_COUNT', { count }); } }, hasSyncEnabled: { @@ -405,7 +403,7 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement // Set properties on the ToggleSwitchComponent instance instance.rowData = rowData; - instance.value = rowData?.repository?.hasSyncEnabled || false; + instance.value = rowData?.customFields?.repository?.hasSyncEnabled || false; // Subscribe to the 'switched' event of the ToggleSwitchComponent instance.switched.subscribe({ @@ -415,8 +413,9 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement this.updateGithubRepository(rowData, hasSyncEnabled); }, // If there is an error, log a warning - error: (err: any) => { - console.warn(err); + error: (error: any) => { + // Handle and log errors using an error handling service + this._errorHandlingService.handleError(error); } }); } @@ -456,9 +455,10 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement componentInitFunction: (instance: StatusBadgeComponent, cell: Cell) => { // Get the data of the entire row const row = cell.getRow().getData(); + const repository: IOrganizationGithubRepository = row.customFields.repository; // Transform the column data using 'this.statusMapper' - instance.value = this.statusMapper(row.repository); + instance.value = this.statusMapper(repository); } } } @@ -471,7 +471,7 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement * @param hasSyncEnabled - A boolean indicating whether sync is enabled. */ private updateGithubRepository(project: IOrganizationProject, hasSyncEnabled: boolean) { - const repository = project['repository']; + const repository = project.customFields['repository']; if (!repository) { return; } @@ -486,18 +486,6 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement organizationId }) .pipe( - tap((response: any) => { - if (response['status'] == HttpStatus.BAD_REQUEST) { - throw new Error(`${response['message']}`); - } - }), - // Catch and handle errors - catchError((error) => { - // Handle and log errors using the _errorHandlingService - this._errorHandlingService.handleError(error); - // Return an empty observable to continue the stream - return EMPTY; - }), // Perform side effects tap(() => { // Determine the success message based on whether hasSyncEnabled is true or false @@ -513,6 +501,13 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement }), // Update the subject with a value of true tap(() => this.subject$.next(true)), + // Catch and handle errors + catchError((error) => { + // Handle and log errors using the _errorHandlingService + this._errorHandlingService.handleError(error); + // Return an empty observable to continue the stream + return EMPTY; + }), // Handle component lifecycle to avoid memory leaks untilDestroyed(this) ) @@ -584,44 +579,44 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement this._githubService .syncGithubRepository(repositorySyncRequest) .pipe( - tap((item: IOrganizationGithubRepository) => (repository = item)), - mergeMap(({ id: repositoryId }: IOrganizationGithubRepository) => - this._organizationProjectsService.updateProjectSetting(projectId, { + tap((item: IOrganizationGithubRepository) => { + repository = item; + }), + mergeMap(({ id: repositoryId }: IOrganizationGithubRepository) => { + const setting$ = this._organizationProjectsService.updateProjectSetting(projectId, { organizationId, tenantId, - repositoryId, + customFields: { repositoryId }, syncTag: SYNC_TAG_GAUZY - }) - ), - tap((response: any) => { - if (response['status'] == HttpStatus.BAD_REQUEST) { - throw new Error(`${response['message']}`); - } - }), - mergeMap(() => - this._githubService.autoSyncIssues(integrationId, repository, { + }); + const issues$ = this._githubService.autoSyncIssues(integrationId, repository, { projectId, organizationId, tenantId - }) - ), - tap((process: boolean) => { - if (process) { - this._toastrService.success( - this.getTranslation('INTEGRATIONS.GITHUB_PAGE.SYNCED_ISSUES', { - repository: this.repository.full_name - }), - this.getTranslation('TOASTR.TITLE.SUCCESS') - ); - } - this.subject$.next(true); + }); + return setting$.pipe( + mergeMap(() => issues$), + tap((process: boolean) => { + if (process) { + this._toastrService.success( + this.getTranslation('INTEGRATIONS.GITHUB_PAGE.SYNCED_ISSUES', { + repository: this.repository.full_name + }), + this.getTranslation('TOASTR.TITLE.SUCCESS') + ); + } + this.subject$.next(true); + }) + ); }), catchError((error) => { this._errorHandlingService.handleError(error); return EMPTY; }), // Execute the following code block when the observable completes or errors - finalize(() => (this.syncing = this.loading = false)), + finalize(() => { + this.syncing = this.loading = false; + }), // Automatically unsubscribe when the component is destroyed untilDestroyed(this) ) @@ -678,7 +673,7 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement this._organizationProjectsService.updateProjectSetting(projectId, { organizationId, tenantId, - repositoryId, + customFields: { repositoryId }, syncTag: SYNC_TAG_GAUZY }) ), @@ -735,14 +730,14 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement resyncIssues(project: IOrganizationProject) { try { // Ensure there is a valid organization, and project - if (!this.organization || !project || !project.repository) { + if (!this.organization || !project || !project?.customFields?.repository) { return; } this.loading = true; this.project = project; - const { repository } = project; + const { repository } = project.customFields; const { id: organizationId, tenantId } = this.organization; const { id: integrationId } = this.integration; @@ -755,11 +750,6 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement tenantId }) .pipe( - tap((response: any) => { - if (response['status'] == HttpStatus.BAD_REQUEST) { - throw new Error(`${response['message']}`); - } - }), tap((process: boolean) => { if (process) { this._toastrService.success( @@ -776,13 +766,19 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement return EMPTY; }), // Execute the following code block when the observable completes or errors - finalize(() => (this.loading = false)), + finalize(() => { + this.loading = false; + }), // Automatically unsubscribe when the component is destroyed untilDestroyed(this) ) .subscribe(); } catch (error) { - console.log(error); + // Handle errors (e.g., display an error message or log the error) + console.log('Error while re-syncing issues from repository:', error.message); + + // Optionally, you can provide error feedback to the user + this._errorHandlingService.handleError(error); } } 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 index 2df16eb37a6..40fd7107099 100644 --- 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 @@ -1,7 +1,7 @@ - +

Installing. Please wait...

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 index 2e337280060..97dd599b67a 100644 --- 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 @@ -25,7 +25,7 @@ export class GithubWizardComponent implements AfterViewInit, OnInit, OnDestroy { // Handle the custom event data here // Set the isLoading property to false, indicating that the loading is complete - this.isLoading = false; + this.loading = false; // Delay the navigation to a specific URL by 100 milliseconds before redirecting // This is often used to provide a smoother user experience @@ -36,7 +36,7 @@ export class GithubWizardComponent implements AfterViewInit, OnInit, OnDestroy { } public organization: IOrganization; - public isLoading: boolean = true; + public loading: boolean = true; // save a reference to the window so we can close it private window = null; private timer: any; @@ -109,14 +109,14 @@ export class GithubWizardComponent implements AfterViewInit, OnInit, OnDestroy { // Get the redirect URI, Post Install URL and client ID from the environment const redirect_uri = environment.GAUZY_GITHUB_REDIRECT_URL; const client_id = environment.GAUZY_GITHUB_CLIENT_ID; - const postInstallURL = environment.GAUZY_GITHUB_POST_INSTALL_URL; + const post_install_url = environment.GAUZY_GITHUB_POST_INSTALL_URL; // Define the query parameters for the authorization request const queryParams = toParams({ redirect_uri: `${redirect_uri}`, client_id: `${client_id}`, scope: 'user', - state: `${postInstallURL}` + state: `${post_install_url}` }); // Construct the external URL for GitHub authorization with the query parameters @@ -187,10 +187,12 @@ export class GithubWizardComponent implements AfterViewInit, OnInit, OnDestroy { // A window with the same name is already open, so focus on it window.frames[windowName].focus(); } else { - // Get the redirect URI, Post Install URL and client ID from the environment - const redirect_uri = environment.GAUZY_GITHUB_REDIRECT_URL; + // Destructure environment variables for better readability + const { GAUZY_GITHUB_APP_NAME, GAUZY_GITHUB_REDIRECT_URL, GAUZY_GITHUB_POST_INSTALL_URL } = environment; + // Get the redirect URI, Post Install URL from the environment + const redirect_uri = GAUZY_GITHUB_REDIRECT_URL; // const client_id = environment.GAUZY_GITHUB_CLIENT_ID; - const postInstallURL = environment.GAUZY_GITHUB_POST_INSTALL_URL; + const postInstallURL = GAUZY_GITHUB_POST_INSTALL_URL; // Define the query parameters for the authorization request const queryParams = toParams({ @@ -200,26 +202,14 @@ export class GithubWizardComponent implements AfterViewInit, OnInit, OnDestroy { // Construct the external URL for GitHub authorization with the query parameters /** Navigate to the target external URL */ - const url = `https://github.com/apps/${ - environment.GAUZY_GITHUB_APP_NAME - }/installations/new?${queryParams.toString()}`; + const url = `https://github.com/apps/${GAUZY_GITHUB_APP_NAME}/installations/new?${queryParams.toString()}`; console.log('External Github App Installation URL: %s', url); /** 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, - ` + `width=${width},height=${height},top=${top},left=${left},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes` ); } } @@ -247,7 +237,7 @@ export class GithubWizardComponent implements AfterViewInit, OnInit, OnDestroy { */ private handleClosedPopupWindow(ms: number = 200): void { // Set isLoading to false to indicate that loading has completed - this.isLoading = false; + this.loading = false; // Delay navigation by 'ms' milliseconds before redirecting setTimeout(() => { diff --git a/apps/gauzy/src/app/pages/jobs/employees/employees/employees.component.ts b/apps/gauzy/src/app/pages/jobs/employees/employees/employees.component.ts index ba541d63b6d..a3a34e4596e 100644 --- a/apps/gauzy/src/app/pages/jobs/employees/employees/employees.component.ts +++ b/apps/gauzy/src/app/pages/jobs/employees/employees/employees.component.ts @@ -9,7 +9,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; import { Cell } from 'angular2-smart-table'; import { IEmployee, IEmployeeJobsStatisticsResponse, IOrganization, ISelectedEmployee } from '@gauzy/contracts'; -import { EmployeesService, ServerDataSource, ToastrService } from '@gauzy/ui-core/core'; +import { EmployeesService, JobService, ServerDataSource, ToastrService } from '@gauzy/ui-core/core'; import { API_PREFIX, Store, distinctUntilChange } from '@gauzy/ui-core/common'; import { EmployeeLinksComponent, @@ -50,6 +50,7 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements private readonly _router: Router, private readonly _store: Store, private readonly _employeesService: EmployeesService, + private readonly _jobService: JobService, private readonly _toastrService: ToastrService, private readonly _currencyPipe: CurrencyPipe ) { @@ -129,7 +130,7 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements // Create a new ServerDataSource for Smart Table this.smartTableSource = new ServerDataSource(this._http, { - endPoint: `${API_PREFIX}/employee/job-statistics`, + endPoint: `${API_PREFIX}/employee-job/statistics`, relations: ['user'], // Define query parameters for the API request where: { @@ -308,8 +309,7 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements } // Destructure properties for clarity. - const { tenantId } = this._store.user; - const { id: organizationId } = this.organization; + const { id: organizationId, tenantId } = this.organization; const employeeId = event.data?.id; const { billRateValue, minimumBillingRate } = event.newData ?? {}; @@ -348,7 +348,7 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements const { id: organizationId, tenantId } = this.organization; // Update the job search status using the employeesService. - await this._employeesService.updateJobSearchStatus(employee.id, { + await this._jobService.updateJobSearchStatus(employee.id, { isJobSearchActive, organizationId, tenantId @@ -359,9 +359,8 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements ? 'TOASTR.MESSAGE.EMPLOYEE_JOB_STATUS_ACTIVE' : 'TOASTR.MESSAGE.EMPLOYEE_JOB_STATUS_INACTIVE'; - this._toastrService.success(toastrMessageKey, { - name: employee.fullName.trim() - }); + const fullName = employee.fullName.trim(); + this._toastrService.success(toastrMessageKey, { name: fullName }); } catch (error) { // Display an error toastr notification in case of any exceptions. this._toastrService.danger(error); @@ -423,11 +422,12 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements } /** + * Navigates to the employee addition page and opens the add dialog. * - * @param event - * @returns + * @param event - The pointer event that triggered this method. + * @returns A promise that resolves when the navigation is complete or exits early if there is no organization. */ - addNew = async (event: PointerEvent) => { + addNew = async (event: PointerEvent): Promise => { if (!this.organization) { return; } @@ -436,7 +436,7 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements queryParams: { openAddDialog: true } }); } catch (error) { - this._toastrService.error(error); + this._toastrService.error(error.message || error); } }; diff --git a/apps/gauzy/src/app/pages/jobs/search/components/apply-job-manually/apply-job-manually.component.ts b/apps/gauzy/src/app/pages/jobs/search/components/apply-job-manually/apply-job-manually.component.ts index 4c7e7c0a1d2..dd2af200825 100644 --- a/apps/gauzy/src/app/pages/jobs/search/components/apply-job-manually/apply-job-manually.component.ts +++ b/apps/gauzy/src/app/pages/jobs/search/components/apply-job-manually/apply-job-manually.component.ts @@ -7,7 +7,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { NbDialogRef } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; import { CKEditor4, CKEditorComponent } from 'ckeditor4-angular'; -import { FileUploader, FileUploaderOptions } from 'ng2-file-upload'; +import { FileItem, FileUploader, FileUploaderOptions } from 'ng2-file-upload'; import { IEmployeeJobApplication, IEmployee, @@ -165,6 +165,9 @@ export class ApplyJobManuallyComponent extends TranslationBaseComponent implemen } ngAfterViewInit() { + this.uploader.onAfterAddingFile = (file) => { + file.withCredentials = false; + }; this.uploader.onSuccessItem = (item: any, response: string, status: number) => { try { if (response) { @@ -196,6 +199,11 @@ export class ApplyJobManuallyComponent extends TranslationBaseComponent implemen } } + /** + * Load settings for the file uploader, including headers and additional form data. + * + * @returns void + */ private _loadUploaderSettings() { if (!this.store.user) { return; @@ -207,20 +215,30 @@ export class ApplyJobManuallyComponent extends TranslationBaseComponent implemen headers.push({ name: 'Authorization', value: `Bearer ${token}` }); headers.push({ name: 'Tenant-Id', value: tenantId }); + if (!!this.organization) { + headers.push({ name: 'Organization-Id', value: `${this.organization.id}` }); + } + const uploaderOptions: FileUploaderOptions = { url: environment.API_BASE_URL + `${API_PREFIX}/image-assets/upload/proposal_attachments`, - // XHR request method - method: 'POST', - // Upload files automatically upon addition to upload queue - autoUpload: true, - // Use xhrTransport in favor of iframeTransport - isHTML5: true, - // Calculate progress independently for each uploaded file - removeAfterUpload: true, - // XHR request headers - headers: headers + method: 'POST', // XHR request method + autoUpload: true, // Upload files automatically upon addition to upload queue + isHTML5: true, // Use xhrTransport in favor of iframeTransport + removeAfterUpload: true, // Calculate progress independently for each uploaded file + headers: headers // XHR request headers }; this.uploader = new FileUploader(uploaderOptions); + + // Adding additional form data + this.uploader.onBuildItemForm = (fileItem: FileItem, form) => { + if (!!this.store.user.tenantId) { + form.append('tenantId', tenantId); + } + + if (!!this.organization) { + form.append('organizationId', this.organization.id); + } + }; } public fileOverBase(e: any): void { diff --git a/apps/gauzy/src/app/pages/jobs/search/search.module.ts b/apps/gauzy/src/app/pages/jobs/search/search.module.ts index 31510928d37..0ad57b168e0 100644 --- a/apps/gauzy/src/app/pages/jobs/search/search.module.ts +++ b/apps/gauzy/src/app/pages/jobs/search/search.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - import { NbButtonModule, NbCardModule, @@ -15,6 +14,7 @@ import { } from '@nebular/theme'; import { Angular2SmartTableModule } from 'angular2-smart-table'; import { MomentModule } from 'ngx-moment'; +import { NgxPermissionsModule } from 'ngx-permissions'; import { I18nTranslateModule } from '@gauzy/ui-core/i18n'; import { DialogsModule, @@ -46,6 +46,7 @@ import { SearchComponent } from './search/search.component'; Angular2SmartTableModule, SearchRoutingModule, I18nTranslateModule.forChild(), + NgxPermissionsModule.forChild(), SharedModule, DialogsModule, EmployeeMultiSelectModule, diff --git a/apps/gauzy/src/app/pages/jobs/search/search/search.component.html b/apps/gauzy/src/app/pages/jobs/search/search/search.component.html index 4fa1ed27ad1..4edcac80166 100644 --- a/apps/gauzy/src/app/pages/jobs/search/search/search.component.html +++ b/apps/gauzy/src/app/pages/jobs/search/search/search.component.html @@ -6,9 +6,7 @@

- + - + - +
- - + + - +
@@ -114,183 +98,93 @@

(keydown.enter)="handleSubmitOnEnter()" >
-
+
-
-
- +
- +
- @@ -437,11 +317,6 @@

- +
diff --git a/apps/gauzy/src/app/pages/jobs/search/search/search.component.ts b/apps/gauzy/src/app/pages/jobs/search/search/search.component.ts index 2986eaece12..4f2dcfb1a21 100644 --- a/apps/gauzy/src/app/pages/jobs/search/search/search.component.ts +++ b/apps/gauzy/src/app/pages/jobs/search/search/search.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, AfterViewInit } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { ActivatedRoute, Data, Router } from '@angular/router'; @@ -11,6 +11,7 @@ import { Cell } from 'angular2-smart-table'; import { AtLeastOneFieldValidator, DateRangePickerBuilderService, + ErrorHandlingService, ProposalTemplateService, ServerDataSource, ToastrService @@ -19,7 +20,6 @@ import { IEmployeeJobApplication, IDateRangePicker, IEmployeeJobPost, - IGetEmployeeJobPostFilters, IJobMatchings, IOrganization, ISelectedEmployee, @@ -40,7 +40,7 @@ import { PaginationFilterBaseComponent, getAdjustDateRangeFutureAllowed } from '@gauzy/ui-core/shared'; -import { API_PREFIX, Store, distinctUntilChange, isEmpty, isNotEmpty, toUTC } from '@gauzy/ui-core/common'; +import { API_PREFIX, Store, distinctUntilChange, isNotEmpty, toUTC } from '@gauzy/ui-core/common'; import { ApplyJobManuallyComponent } from '../components'; import { JobTitleDescriptionDetailsComponent } from '../../table-components'; @@ -50,36 +50,24 @@ import { JobTitleDescriptionDetailsComponent } from '../../table-components'; templateUrl: './search.component.html', styleUrls: ['./search.component.scss'] }) -export class SearchComponent extends PaginationFilterBaseComponent implements OnInit, OnDestroy, AfterViewInit { +export class SearchComponent extends PaginationFilterBaseComponent implements AfterViewInit, OnInit, OnDestroy { loading: boolean = false; isRefresh: boolean = false; autoRefresh: boolean = false; settingsSmartTable: object; isOpenAdvancedFilter: boolean = false; jobs: IEmployeeJobPost[] = []; - JobPostSourceEnum = JobPostSourceEnum; JobPostTypeEnum = JobPostTypeEnum; JobPostStatusEnum = JobPostStatusEnum; PermissionsEnum = PermissionsEnum; JobSearchTabsEnum = JobSearchTabsEnum; - - jobRequest: IGetEmployeeJobPostFilters = { - employeeIds: [], - jobSource: [], - jobType: [], - jobStatus: null, - budget: [] - }; - jobs$: Subject = this.subject$; smartTableSource: ServerDataSource; autoRefreshTimer: Subscription; disableButton: boolean = true; selectedJob: IEmployeeJobPost; - nbTab$: Subject = new BehaviorSubject(JobSearchTabsEnum.ACTIONS); - public organization: IOrganization; public selectedEmployee: ISelectedEmployee; public selectedDateRange: IDateRangePicker; @@ -87,7 +75,7 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On /* * Search Tab Form */ - public form: UntypedFormGroup = SearchComponent.buildForm(this.fb); + public form: UntypedFormGroup = SearchComponent.buildForm(this._fb); static buildForm(fb: UntypedFormBuilder): UntypedFormGroup { return fb.group( { @@ -104,17 +92,18 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On } constructor( - private readonly fb: UntypedFormBuilder, - private readonly http: HttpClient, + public readonly translateService: TranslateService, + private readonly _fb: UntypedFormBuilder, + private readonly _http: HttpClient, private readonly _activatedRoute: ActivatedRoute, private readonly _router: Router, - private readonly dialogService: NbDialogService, - private readonly store: Store, - public readonly translateService: TranslateService, - public readonly proposalTemplateService: ProposalTemplateService, - private readonly toastrService: ToastrService, - private readonly jobService: JobService, - private readonly dateRangePickerBuilderService: DateRangePickerBuilderService + private readonly _dialogService: NbDialogService, + private readonly _store: Store, + private readonly _proposalTemplateService: ProposalTemplateService, + private readonly _toastrService: ToastrService, + private readonly _jobService: JobService, + private readonly _dateRangePickerBuilderService: DateRangePickerBuilderService, + private readonly _errorHandlingService: ErrorHandlingService ) { super(translateService); @@ -176,9 +165,9 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On } ngAfterViewInit(): void { - const storeOrganization$ = this.store.selectedOrganization$; - const storeEmployee$ = this.store.selectedEmployee$; - const selectedDateRange$ = this.dateRangePickerBuilderService.selectedDateRange$; + const storeOrganization$ = this._store.selectedOrganization$; + const storeEmployee$ = this._store.selectedEmployee$; + const selectedDateRange$ = this._dateRangePickerBuilderService.selectedDateRange$; combineLatest([storeOrganization$, selectedDateRange$, storeEmployee$]) .pipe( debounceTime(100), @@ -188,7 +177,6 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On this.organization = organization; this.selectedDateRange = dateRange; this.selectedEmployee = employee && employee.id ? employee : null; - this.jobRequest.employeeIds = this.selectedEmployee ? [this.selectedEmployee.id] : []; }), tap(() => this._loadSmartTableSettings()), tap(() => this.jobs$.next(true)), @@ -197,17 +185,23 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On .subscribe(); } - /** Get employee default proposal template */ - async getEmployeeDefaultProposalTemplate(job: IJobMatchings) { + /** + * Retrieves the default proposal template for the specified employee and organization. + * @param {IJobMatchings} job - The job matching object containing employeeId. + * @returns {Promise} A promise resolving to the default proposal template or null if not found. + */ + async getEmployeeDefaultProposalTemplate(job: IJobMatchings): Promise { + // Check if organization context is available if (!this.organization) { - return; + return null; } - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; + // Extract necessary IDs + const { id: organizationId, tenantId } = this.organization; const { employeeId } = job; - const { items = [] } = await this.proposalTemplateService.getAll({ + // Retrieve proposal templates matching criteria + const { items = [] } = await this._proposalTemplateService.getAll({ where: { tenantId, organizationId, @@ -215,20 +209,25 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On isDefault: true } }); + + // Return the first matching default template or null if not found return items.length > 0 ? items[0] : null; } - copyTextToClipboard(text) { + /** + * Copies the given text to the clipboard. + * @param {string} text - The text to be copied to the clipboard. + * @returns {Promise} A promise that resolves when the text is copied. + */ + async copyTextToClipboard(text: string): Promise { if (!navigator.clipboard) { + // Fallback method for older browsers that do not support navigator.clipboard API const textArea = document.createElement('textarea'); textArea.value = text; // Avoid scrolling to bottom - textArea.style.width = '0'; - textArea.style.height = '0'; - textArea.style.top = '0'; - textArea.style.left = '0'; textArea.style.position = 'fixed'; + textArea.style.opacity = '0'; // Make textarea invisible document.body.appendChild(textArea); textArea.focus(); @@ -236,42 +235,53 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On try { const successful = document.execCommand('copy'); - const msg = successful ? 'successful' : 'unsuccessful'; - console.log('Fallback: Copying text command was ' + msg); - } catch (err) { - console.error('Fallback: Oops, unable to copy', err); + if (!successful) { + throw new Error('Fallback: Copy command was unsuccessful'); + } + console.log('Fallback: Copying text command was successful'); + } catch (error) { + console.error('Fallback: Oops, unable to copy', error); + throw new Error(`Fallback: Copy command was unsuccessful: ${error?.message}`); + } finally { + document.body.removeChild(textArea); // Clean up } - return; - } - return navigator.clipboard.writeText(text).then( - () => { + } else { + // Modern method using navigator.clipboard API + try { + await navigator.clipboard.writeText(text); console.log('Async: Copying to clipboard was successful!'); - }, - (err) => { - console.error('Async: Could not copy text: ', err); + } catch (error) { + console.error('Async: Could not copy text: ', error); + throw new Error(`Async: Could not copy text: ${error?.message}`); } - ); + } } - setAutoRefresh(value: boolean) { + /** + * Sets the auto refresh behavior based on the provided value. + * @param {boolean} value - If true, enables auto refresh; if false, disables it. + */ + setAutoRefresh(value: boolean): void { if (value) { - this.autoRefreshTimer = timer(0, 60000) + // Enable auto refresh + this.autoRefreshTimer = timer(0, 60000) // Timer starts immediately and fires every 60 seconds .pipe( - tap(() => this.refresh()), - untilDestroyed(this) + tap(() => this.refresh()), // Perform the refresh action on each timer tick + untilDestroyed(this) // Automatically unsubscribe when component is destroyed ) .subscribe(); } else { - if (this.autoRefreshTimer) { - this.autoRefreshTimer.unsubscribe(); + // Disable auto refresh + if (this.autoRefreshTimer instanceof Subscription) { + this.autoRefreshTimer.unsubscribe(); // Unsubscribe from the timer observable + this.autoRefreshTimer = null; // Clear the timer reference } } } /** - * Custom events - * - * @param $event + * Handles custom events related to job actions such as viewing, applying, and hiding jobs. + * @param $event The custom event containing action and data payload. */ async onCustomEvents($event: { action: string; data: any }) { switch ($event.action) { @@ -281,24 +291,38 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On } break; case 'apply': + // Define the applyRequest object const applyRequest: IEmployeeJobApplication = { applied: true, employeeId: $event.data.employeeId, providerCode: $event.data.providerCode, providerJobId: $event.data.providerJobId }; - this.jobService.applyJob(applyRequest).then(async (resp) => { - this.toastrService.success('TOASTR.MESSAGE.JOB_APPLIED'); + + try { + // Await the applyJob function call + const resp = await this._jobService.applyJob(applyRequest); + + // Show success message and refresh smart table + this._toastrService.success('TOASTR.MESSAGE.JOB_APPLIED'); this.smartTableSource.refresh(); + // Check if a redirect is required if (resp.isRedirectRequired) { + // Fetch the proposal template const proposalTemplate = await this.getEmployeeDefaultProposalTemplate($event.data); if (proposalTemplate) { + // Copy proposal content to clipboard await this.copyTextToClipboard(proposalTemplate.content); } + // Open a new window with job post URL window.open($event.data.jobPost.url, '_blank'); } - }); + } catch (error) { + console.error('Error while applying job:', error); + // Optionally show an error message or handle the error scenario + this._errorHandlingService.handleError(error); + } break; case 'hide': try { @@ -309,10 +333,12 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On providerJobId: $event.data.providerJobId }); - this.toastrService.success('TOASTR.MESSAGE.JOB_HIDDEN'); + this._toastrService.success('TOASTR.MESSAGE.JOB_HIDDEN'); this.smartTableSource.refresh(); } catch (error) { console.log('Error while hide job', error); + // Optionally show an error message or handle the error scenario + this._errorHandlingService.handleError(error); } break; default: @@ -330,109 +356,141 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On this.selectedJob = isSelected ? data : null; } - public viewJob() { + /** + * Opens the job post URL in a new tab if a job is selected and has a valid URL. + */ + public viewJob(): void { if (!this.selectedJob) { return; } - if (this.selectedJob.jobPost) { + + if (this.selectedJob.jobPost && this.selectedJob.jobPost.url) { window.open(this.selectedJob.jobPost.url, '_blank'); } } /** - * Updates job visibility - * - * @returns + * Updates job visibility by hiding the selected job post. + * Displays success message on job hidden and refreshes the smart table source. */ - public async hideJob() { + public async hideJob(): Promise { + // Check if a job is selected if (!this.selectedJob) { return; } try { + // Destructure selected job properties const { employeeId, providerCode, providerJobId } = this.selectedJob; + + // Call service method to hide the job post await this.hideJobPost({ hide: true, employeeId, providerCode, providerJobId }); - this.toastrService.success('TOASTR.MESSAGE.JOB_HIDDEN'); + // Display success message using toastr service + this._toastrService.success('TOASTR.MESSAGE.JOB_HIDDEN'); + + // Refresh the smart table source this.smartTableSource.refresh(); + // Clear selection of the job post this.onSelectJob({ isSelected: false, data: null }); } catch (error) { - console.log('Error while hide job', error); + // Log and handle any errors that occur during hiding the job post + console.error('Error while hiding job', error); + this._toastrService.error('TOASTR.MESSAGE.ERROR_HIDING_JOB'); } } /** - * Updates job visibility + * Updates job visibility by hiding the job post based on the provided input. * - * @param input + * @param input The input data containing employee ID, provider code, and provider job ID. */ - public async hideJobPost(input: IVisibilityJobPostInput) { + public async hideJobPost(input: IVisibilityJobPostInput): Promise { try { const { employeeId, providerCode, providerJobId } = input; + + // Check if provider code and provider job ID are provided if (providerCode && providerJobId) { + // Prepare payload for hiding job post const payload: IVisibilityJobPostInput = { hide: true, - employeeId: employeeId, - providerCode: providerCode, - providerJobId: providerJobId + employeeId, + providerCode, + providerJobId }; - await this.jobService.hideJob(payload); + + // Call job service method to hide the job post + await this._jobService.hideJob(payload); } } catch (error) { - console.log('Error while hide job', error); + // Log and handle any errors that occur during hiding the job post + console.error('Error while hiding job', error); } } /** - * Already applied job from provider site - * - * @returns + * Marks the selected job as already applied on the provider site. + * Updates job application status and refreshes the smart table source. */ - async appliedJob() { + async appliedJob(): Promise { + // Check if a job is selected if (!this.selectedJob) { return; } + try { + // Destructure selected job properties const { employeeId, providerCode, providerJobId } = this.selectedJob; - await this.jobService.updateApplied({ + + // Call job service method to update job application status + await this._jobService.updateApplied({ employeeId, providerCode, providerJobId, applied: true }); - this.toastrService.success('TOASTR.MESSAGE.JOB_APPLIED'); + // Display success message using toastr service + this._toastrService.success('TOASTR.MESSAGE.JOB_APPLIED'); + + // Refresh the smart table source this.smartTableSource.refresh(); } catch (error) { - console.log('Error while applied job', error); + // Log and handle any errors that occur during updating job application status + console.error('Error while marking job as applied', error); + this._toastrService.error('TOASTR.MESSAGE.ERROR_APPLYING_JOB'); } } /** - * Apply For Job Post + * Apply for a job post using the provided job application details. * - * @param applyJobPost - * @returns + * @param applyJobPost The job application details. */ async applyToJob(applyJobPost: IEmployeeJobApplication): Promise { + // Check if a job is selected if (!this.selectedJob) { return; } try { - const appliedJob = await this.jobService.applyJob(applyJobPost); - this.toastrService.success('TOASTR.MESSAGE.JOB_APPLIED'); + // Apply for the job using job service method + const appliedJob = await this._jobService.applyJob(applyJobPost); - // removed selected row from table after applied + // Display success message using toastr service + this._toastrService.success('TOASTR.MESSAGE.JOB_APPLIED'); + + // Remove the selected row from the table after applying const row = document.querySelector('angular2-smart-table > table > tbody > .angular2-smart-row.selected'); - if (!!row) { + if (row) { row.remove(); this.onSelectJob({ isSelected: false, data: null }); } + // Handle redirection and proposal copying if required if (appliedJob.isRedirectRequired) { - // If we have generated proposal, let's copy to clipboard + // Copy proposal to clipboard if generated, else use default proposal template if (appliedJob.proposal) { await this.copyTextToClipboard(appliedJob.proposal); } else { @@ -441,46 +499,55 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On await this.copyTextToClipboard(proposalTemplate.content); } } + // Open job post URL in a new tab window.open(this.selectedJob.jobPost.url, '_blank'); } } catch (error) { - console.log('Error while applying job post', error); + // Log and handle any errors that occur during job application + console.error('Error while applying job post', error); + this._toastrService.error('TOASTR.MESSAGE.ERROR_APPLYING_JOB'); } } - /** Apply For Job Automatically */ + /** + * Apply for a job automatically using the selected job details. + */ async applyToJobAutomatically() { + // Check if a job is selected if (!this.selectedJob) { return; } + try { + // Prepare job application details const { providerCode, providerJobId, employeeId } = this.selectedJob; const applyJobPost: IEmployeeJobApplication = { applied: true, - ...(isNotEmpty(this.selectedEmployee) - ? { - employeeId: this.selectedEmployee.id - } - : { - employeeId - }), + // Choose employeeId based on whether selectedEmployee is defined + ...(this.selectedEmployee?.id ? { employeeId: this.selectedEmployee.id } : { employeeId }), providerCode, providerJobId }; + // Apply for the job using applyToJob method await this.applyToJob(applyJobPost); } catch (error) { - console.log('Error while applying job post automatically', error); + // Log and handle any errors that occur during automatic job application + console.error('Error while applying job post automatically', error); } } - /** Apply For Job Manually */ + /** + * Apply for a job manually using a dialog component. + */ async applyToJobManually() { + // Check if a job is selected if (!this.selectedJob) { return; } - const dialog = this.dialogService.open(ApplyJobManuallyComponent, { + // Open a dialog to handle manual job application + const dialog = this._dialogService.open(ApplyJobManuallyComponent, { context: { employeeJobPost: this.selectedJob, selectedEmployee: this.selectedEmployee @@ -488,14 +555,16 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On hasScroll: false }); - const result = await firstValueFrom(dialog.onClose); - - if (result) { - const { providerCode, providerJobId } = this.selectedJob; + try { + // Wait for dialog result + const result = await firstValueFrom(dialog.onClose); - const { applied, employeeId, proposal, rate, details, attachments } = result; + // Process job application if result is available + if (result) { + const { providerCode, providerJobId } = this.selectedJob; + const { applied, employeeId, proposal, rate, details, attachments } = result; - try { + // Prepare job application details const applyJobPost: IEmployeeJobApplication = { applied, employeeId, @@ -507,15 +576,18 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On providerJobId }; + // Apply for the job using applyToJob method await this.applyToJob(applyJobPost); - } catch (error) { - console.log('Error while applying job post manually', error); } + } catch (error) { + // Log and handle any errors that occur during manual job application + console.error('Error while applying job post manually', error); } } private _loadSmartTableSettings() { const self: SearchComponent = this; + console.log(this.selectedEmployee?.id); const pagination: IPaginationBase = this.getPagination(); this.settingsSmartTable = { @@ -528,8 +600,9 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On perPage: pagination ? pagination.itemsPerPage : 10 }, columns: { - ...(isEmpty(this.selectedEmployee) - ? { + ...(this.selectedEmployee?.id + ? {} + : { employee: { title: this.getTranslation('JOBS.EMPLOYEE'), filter: false, @@ -540,6 +613,7 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On componentInitFunction: (instance: EmployeeLinksComponent, cell: Cell) => { const employee: IEmployee = cell.getRawValue() as IEmployee; instance.rowData = cell.getRow().getData(); + instance.value = { name: employee?.user?.name ?? null, imageUrl: employee?.user?.imageUrl ?? null, @@ -547,8 +621,7 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On }; } } - } - : {}), + }), jobDetails: { title: this.getTranslation('JOBS.JOB_DETAILS'), width: '85%', @@ -579,7 +652,7 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On /** * Initiate smart table source configuration */ - this.smartTableSource = new ServerDataSource(this.http, { + this.smartTableSource = new ServerDataSource(this._http, { endPoint: `${API_PREFIX}/employee-job`, pagerPageKey: 'page', pagerLimitKey: 'limit', @@ -596,6 +669,10 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On } } + /** + * + * @returns + */ private async getEmployeesJob() { if (!this.organization) { return; @@ -630,15 +707,14 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On } ] : []), - ...(isNotEmpty(this.selectedEmployee) + ...(isNotEmpty(this.selectedEmployee?.id) ? [ { field: 'employeeIds', - search: [this.selectedEmployee.id] + search: [this.selectedEmployee?.id] } ] : []), - ...(startDate && endDate ? [ { @@ -721,56 +797,81 @@ export class SearchComponent extends PaginationFilterBaseComponent implements On */ this.smartTableSource.setPaging(activePage, itemsPerPage, false); } catch (error) { - this.toastrService.danger(error); + this._toastrService.danger(error); } } - private _applyTranslationOnSmartTable() { - this.translateService.onLangChange - .pipe( - tap(() => this._loadSmartTableSettings()), - untilDestroyed(this) - ) - .subscribe(); - } - /* * Hide all jobs */ - hideAll() { + async hideAll() { const request: IVisibilityJobPostInput = { hide: true, ...(isNotEmpty(this.selectedEmployee) ? { employeeId: this.selectedEmployee.id } : {}) }; - this.jobService.hideJob(request).then(() => { - this.toastrService.success('TOASTR.MESSAGE.JOB_HIDDEN'); + + try { + await this._jobService.hideJob(request); + this._toastrService.success('TOASTR.MESSAGE.JOB_HIDDEN'); this.smartTableSource.refresh(); - }); + } catch (error) { + console.log('Error while hiding jobs:', error); + // Handle and log errors using an error handling service + this._errorHandlingService.handleError(error); + } } - onTabChange(tab: NbTabComponent) { + private _applyTranslationOnSmartTable() { + this.translateService.onLangChange + .pipe( + tap(() => this._loadSmartTableSettings()), + untilDestroyed(this) + ) + .subscribe(); + } + + /** + * Handles tab change event. + * Resets the form and updates the active tab ID. + * @param tab The tab component that triggered the change. + */ + onTabChange(tab: NbTabComponent): void { this.form.reset(); this.nbTab$.next(tab.tabId); } - searchJobs() { + /** + * Initiates a job search based on form validity. + * Emits a signal to start fetching jobs if the form is valid. + */ + searchJobs(): void { if (this.form.invalid) { return; } this.jobs$.next(true); } - /** Submit form enter key */ - handleSubmitOnEnter() { + /** + * Handles form submission on Enter key press. + * Initiates a job search. + */ + handleSubmitOnEnter(): void { this.searchJobs(); } - reset() { + /** + * Resets the form, clears filters, and refreshes the job list. + */ + reset(): void { this.form.reset(); this._filters = {}; this.refresh(); } + /** + * Initiates a refresh of job list with updated parameters. + * Resets pagination, triggers job fetch, and scrolls to top of page. + */ public refresh(): void { this.isRefresh = true; this.pagination = { diff --git a/apps/gauzy/src/app/pages/jobs/table-components/job-status/job-status.component.ts b/apps/gauzy/src/app/pages/jobs/table-components/job-status/job-status.component.ts index 01d020fc64b..1724ba98e41 100644 --- a/apps/gauzy/src/app/pages/jobs/table-components/job-status/job-status.component.ts +++ b/apps/gauzy/src/app/pages/jobs/table-components/job-status/job-status.component.ts @@ -15,8 +15,7 @@ export class JobStatusComponent extends TranslationBaseComponent { } @Input() rowData: any; - - value: string; + @Input() value: string; /** * Get job status text and class diff --git a/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.html b/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.html index 842845c4ec2..c7246e2ec95 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.html +++ b/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.html @@ -1,13 +1,11 @@
- +

{{ - (dealId - ? 'PIPELINE_DEAL_EDIT_PAGE.HEADER' - : 'PIPELINE_DEAL_CREATE_PAGE.HEADER' - ) | translate: pipeline + ((deal$ | async) ? 'PIPELINE_DEAL_EDIT_PAGE.HEADER' : 'PIPELINE_DEAL_CREATE_PAGE.HEADER') + | translate : pipeline }}

@@ -17,101 +15,76 @@

- +

- + {{ stage.name }}
-
- - - {{ cl.name }} + + + {{ client.name }}
-
- - {{ pr }} + + {{ probability }}
- - - + + + + + + diff --git a/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.ts b/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.ts index 7d2a3a6a533..b3daa54972b 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.ts +++ b/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deal-form/pipeline-deal-form.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Data, Router } from '@angular/router'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { catchError, map, Observable, of, switchMap, tap } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { Subject } from 'rxjs'; -import { IPipeline, IContact } from '@gauzy/contracts'; -import { AppStore, Store } from '@gauzy/ui-core/common'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; +import { IPipeline, IContact, IOrganization, IDeal, IPagination } from '@gauzy/contracts'; +import { distinctUntilChange, Store } from '@gauzy/ui-core/common'; import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; -import { DealsService, OrganizationContactService, PipelinesService, ToastrService } from '@gauzy/ui-core/core'; +import { DealsService, ErrorHandlingService, OrganizationContactService, ToastrService } from '@gauzy/ui-core/core'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -17,184 +17,183 @@ import { DealsService, OrganizationContactService, PipelinesService, ToastrServi styleUrls: ['./pipeline-deal-form.component.scss'] }) export class PipelineDealFormComponent extends TranslationBaseComponent implements OnInit, OnDestroy { - form: UntypedFormGroup; - pipeline: IPipeline; - clients: IContact[]; - selectedClient: IContact; - probabilities = [0, 1, 2, 3, 4, 5]; - selectedProbability: number; - mode: 'CREATE' | 'EDIT' = 'CREATE'; - dealId: string; - pipelineId: string; - - private readonly $akitaPreUpdate: AppStore['akitaPreUpdate']; - private _ngDestroy$ = new Subject(); - private organizationId: string; - private tenantId: string; + public selectedClient: IContact; + public probabilities = [0, 1, 2, 3, 4, 5]; + public selectedProbability: number; + public organization: IOrganization; + public deal: IDeal; + public deal$: Observable; + public pipeline: IPipeline; + public pipeline$: Observable; + public clients: IContact[] = []; + public clients$: Observable; + + // Form Builder + public form: UntypedFormGroup = PipelineDealFormComponent.buildForm(this._fb); + static buildForm(fb: UntypedFormBuilder): UntypedFormGroup { + return fb.group({ + stageId: [null, Validators.required], + title: [null, Validators.required], + clientId: [null], + probability: [null, Validators.required] + }); + } constructor( public readonly translateService: TranslateService, - private readonly router: Router, - private readonly fb: UntypedFormBuilder, - private readonly appStore: AppStore, - private readonly store: Store, - private readonly dealsService: DealsService, - private readonly activatedRoute: ActivatedRoute, - private readonly pipelinesService: PipelinesService, - private readonly clientsService: OrganizationContactService, - private readonly toastrService: ToastrService + private readonly _router: Router, + private readonly _fb: UntypedFormBuilder, + private readonly _store: Store, + private readonly _dealsService: DealsService, + private readonly _activatedRoute: ActivatedRoute, + private readonly _clientsService: OrganizationContactService, + private readonly _toastrService: ToastrService, + private readonly _errorHandlingService: ErrorHandlingService ) { super(translateService); - - this.$akitaPreUpdate = appStore.akitaPreUpdate; - - appStore.akitaPreUpdate = (previous, next) => { - if (previous.user !== next.user) { - setTimeout(() => this.form.patchValue({ createdByUserId: next.user.id })); - } - - return this.$akitaPreUpdate(previous, next); - }; } ngOnInit() { - this._initializeForm(); - this.activatedRoute.params - .pipe( - filter((params) => !!params), - untilDestroyed(this) - ) - .subscribe(async ({ pipelineId, dealId }) => { - this.form.disable(); - if (pipelineId) { - this.pipelineId = pipelineId; - this.mode = 'EDIT'; - } - if (dealId) { - this.dealId = dealId; - } - this.form.enable(); - }); - this.store.selectedOrganization$ - .pipe( - filter((organization) => !!organization), - untilDestroyed(this) - ) - .subscribe(async (org) => { - this.organizationId = org.id; - this.tenantId = this.store.user.tenantId; - - await this.getOrganizationContact(); - - if (this.pipelineId) { - await this.getPipelines(); - } - if (this.dealId) { - await this.getDeal(); - } - }); + // Setting up the organization$ observable pipeline + this.clients$ = this._store.selectedOrganization$.pipe( + // Ensure only distinct values are emitted + distinctUntilChange(), + // Exclude falsy values from the emitted values + filter((organization: IOrganization) => !!organization), + // Tap operator for side effects - setting the organization property + tap((organization: IOrganization) => (this.organization = organization)), + // Switch to route data stream once organization is confirmed + switchMap(() => { + // Extract organization properties + const { id: organizationId, tenantId } = this.organization; + // Fetch contacts + return this._clientsService.getAll([], { + organizationId, + tenantId + }); + }), + // Map the contacts to the clients property + map(({ items }: IPagination) => items), + // Handle errors + catchError((error) => { + console.error('Error fetching organization contacts:', error); + // Handle and log errors + this._errorHandlingService.handleError(error); + return of([]); + }), + // Handle component lifecycle to avoid memory leaks + untilDestroyed(this) + ); + this.pipeline$ = this._activatedRoute.params.pipe( + // Filter for the presence of pipelineId in route params + filter(({ pipelineId }) => !!pipelineId), + // Switch to route data stream once pipelineId is confirmed + switchMap(() => this._activatedRoute.data), + // Exclude falsy values from the emitted values + filter(({ pipeline }: Data) => !!pipeline), + // Map the pipeline to the pipeline property + map(({ pipeline }: Data) => pipeline), + // Tap operator for side effects - setting the form property + tap((pipeline: IPipeline) => { + this.pipeline = pipeline; + this.form.patchValue({ stageId: this.pipeline.stages[0]?.id }); + }), + // Handle component lifecycle to avoid memory leaks + untilDestroyed(this) + ); + this.deal$ = this._activatedRoute.params.pipe( + // Filter for the presence of dealId in route params + filter(({ dealId }) => !!dealId), + // Switch to route data stream once dealId is confirmed + switchMap(() => this._activatedRoute.data), + // Exclude falsy values from the emitted values + filter(({ deal }: Data) => !!deal), + // Map the deal to the deal property + map(({ deal }: Data) => deal), + // Tap operator for side effects - setting the form property + tap((deal: IDeal) => { + this.deal = deal; + this.patchFormValue(deal); + }), + // Handle component lifecycle to avoid memory leaks + untilDestroyed(this) + ); } - private _initializeForm() { - this.form = this.fb.group({ - createdByUserId: [null, Validators.required], - stageId: [null, Validators.required], - title: [null, Validators.required], - clientId: [null], - probability: [null, Validators.required] - }); + /** + * Patch form values with the deal data + * + * @param deal The deal object containing data to patch into the form + */ + patchFormValue(deal: IDeal) { + const { title, stageId, createdBy, probability, clientId } = deal; this.form.patchValue({ - createdByUserId: this.appStore.getValue().user?.id + title, + stageId, + createdBy, + probability, + clientId }); + this.selectedProbability = probability; } - async getPipelines() { - const { tenantId } = this; - await this.pipelinesService - .getAll(['stages'], { - id: this.pipelineId, - tenantId - }) - .then(({ items: [value] }) => (this.pipeline = value)); + /** + * Submits the form data for creating or updating a deal. + * + * This method handles the submission of form data for either creating a new deal + * or updating an existing one. It also manages form state (disable/enable) and + * displays success notifications. + * + * @returns {Promise} A promise that resolves when the form submission is complete. + */ + public async onSubmit(): Promise { + const { organization, form } = this; - this.form.patchValue({ stageId: this.pipeline.stages[0]?.id }); - } + // If no organization is selected, do not proceed + if (!organization) { + return; + } - async getDeal() { - const { tenantId } = this; - await this.dealsService - .getOne(this.dealId, { tenantId }, ['client']) - .then(({ title, stageId, createdBy, probability, clientId, client }) => { - this.form.patchValue({ - title, - stageId, - createdBy, - probability, - clientId - }); - this.selectedProbability = probability; - }); - } + // Extract organizationId and tenantId from the selected organization + const { id: organizationId, tenantId } = organization; - async getOrganizationContact() { - await this.clientsService - .getAll([], { - organizationId: this.organizationId, - tenantId: this.tenantId - }) - .then((res) => (this.clients = res.items)); - } + // Merge the form values with organizationId and tenantId + const value = { ...form.value, organizationId, tenantId }; - public async onSubmit(): Promise { - const { - dealId, - activatedRoute: relativeTo, - form: { value } - } = this; - - this.form.disable(); - await (this.dealId - ? this.dealsService.update( - this.dealId, - Object.assign( - { - organizationId: this.organizationId, - tenantId: this.tenantId - }, - value - ) - ) - : this.dealsService.create( - Object.assign( - { - organizationId: this.organizationId, - tenantId: this.tenantId - }, - value - ) - ) - ) - .then(() => { - if (this.dealId) { - this.toastrService.success('PIPELINE_DEALS_PAGE.DEAL_EDITED', { - name: value.title - }); - } else { - this.toastrService.success('PIPELINE_DEALS_PAGE.DEAL_ADDED', { - name: value.title - }); - } - this.router.navigate([dealId ? '../..' : '..'], { relativeTo }); - }) - .catch(() => this.form.enable()); + // Disable the form to prevent further input during submission + form.disable(); + + try { + // Determine whether to create a new deal or update an existing one + if (this.deal) { + await this._dealsService.update(this.deal?.id, value); + } else { + await this._dealsService.create(value); + } + + // Determine the success message based on whether it's a create or update operation + const successMessage = this.deal?.id ? 'PIPELINE_DEALS_PAGE.DEAL_EDITED' : 'PIPELINE_DEALS_PAGE.DEAL_ADDED'; + + // Display a success notification with the deal title + this._toastrService.success(successMessage, { name: value.title }); + + // Navigate to the appropriate route after successful submission + this._router.navigate([this.deal?.id ? '../..' : '..'], { relativeTo: this._activatedRoute }); + } catch (error) { + // Handle and log errors + this._errorHandlingService.handleError(error); + } finally { + // If an error occurs, re-enable the form for further input + form.enable(); + } } + /** + * Cancels the form submission. + */ cancel() { window.history.back(); } - ngOnDestroy() { - this._ngDestroy$.next(); - this._ngDestroy$.complete(); - } + ngOnDestroy() {} } diff --git a/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deals.component.html b/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deals.component.html index 04c46ec2722..0903a683cad 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deals.component.html +++ b/apps/gauzy/src/app/pages/pipelines/pipeline-deals/pipeline-deals.component.html @@ -1,15 +1,9 @@ - +

- - {{ 'PIPELINE_DEALS_PAGE.HEADER' | translate }} - - | - - {{ pipeline?.name }} - + {{ 'PIPELINE_DEALS_PAGE.HEADER' | translate }} | {{ pipeline?.name }}

@@ -25,8 +19,8 @@

@@ -38,37 +32,38 @@

- + + +
- +
+
@@ -78,8 +73,8 @@

- - + + + + + + diff --git a/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts b/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts index d86e1aeb439..2d6210349d6 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts +++ b/apps/gauzy/src/app/pages/pipelines/pipeline-form/pipeline-form.component.ts @@ -1,72 +1,119 @@ import { Component, Input, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { filter, tap } from 'rxjs/operators'; import { NbDialogRef } from '@nebular/theme'; -import { IOrganization, IPipeline, IPipelineCreateInput } from '@gauzy/contracts'; -import { PipelinesService } from '@gauzy/ui-core/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { IOrganization, IPipeline } from '@gauzy/contracts'; +import { ErrorHandlingService, PipelinesService } from '@gauzy/ui-core/core'; +import { distinctUntilChange, Store } from '@gauzy/ui-core/common'; +@UntilDestroy({ checkProperties: true }) @Component({ templateUrl: './pipeline-form.component.html', styleUrls: ['./pipeline-form.component.scss'], - selector: 'ga-pipeline-form' + selector: 'ga-pipeline-mutation-form' }) export class PipelineFormComponent implements OnInit { - @Input() pipeline: IPipelineCreateInput & { id?: string }; + public isActive: boolean = true; + public organization: IOrganization; - form: UntypedFormGroup; - icon: string; - isActive: boolean; - organization: IOrganization; + /** + * Form property setter and getter. + */ + public form: UntypedFormGroup = this._fb.group({ + name: ['', Validators.required], + description: [''], + stages: this._fb.array([]), + isActive: [this.isActive] + }); + + /** + * Pipeline property setter and getter. + * @param value + */ + private _pipeline: IPipeline; + @Input() set pipeline(value: IPipeline) { + this._pipeline = value; + this.onPipelineChange(value); + } + get pipeline(): IPipeline { + return this._pipeline; + } constructor( - public readonly dialogRef: NbDialogRef, - private readonly pipelinesService: PipelinesService, - private readonly fb: UntypedFormBuilder + private readonly _dialogRef: NbDialogRef, + private readonly _pipelinesService: PipelinesService, + private readonly _fb: UntypedFormBuilder, + private readonly _store: Store, + private readonly _errorHandlingService: ErrorHandlingService ) {} ngOnInit(): void { - const { id, isActive } = this.pipeline; - isActive === undefined ? (this.isActive = true) : (this.isActive = isActive); + this._store.selectedOrganization$ + .pipe( + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => (this.organization = organization)), + untilDestroyed(this) + ) + .subscribe(); + } - this.form = this.fb.group({ - organizationId: [this.pipeline.organizationId || '', Validators.required], - tenantId: [this.pipeline.tenantId || ''], - name: [this.pipeline.name || '', Validators.required], - ...(id ? { id: [id, Validators.required] } : {}), - description: [this.pipeline.description], - stages: this.fb.array([]), - isActive: [this.isActive] + /** + * Handles changes to the pipeline input. + * @param value The new pipeline value + */ + private onPipelineChange(pipeline: IPipeline): void { + this.isActive = pipeline.isActive ?? true; + + // Patch form values with the new pipeline data + this.form.patchValue({ + name: pipeline.name, + description: pipeline.description, + isActive: this.isActive, + stages: pipeline.stages }); } /** - * + * Closes the dialog. + */ + closeDialog() { + this._dialogRef.close(); + } + + /** + * Toggles the isActive property between true and false. */ setIsActive() { this.isActive = !this.isActive; } /** - * + * Persists the form data by either creating a new entity or updating an existing one. + * This method handles the dialog closure and error logging as well. */ async persist(): Promise { - try { - const { - value, - value: { id } - } = this.form; - let entity: IPipeline; + if (!this.organization) { + return; + } + // Destructure the organization details and form value + const { id: organizationId, tenantId } = this.organization; + const value = { ...this.form.value, organizationId, tenantId, isArchived: !this.isActive }; + + try { // Determine whether to create or update based on the presence of an ID - if (id) { - entity = await this.pipelinesService.update(id, value); - } else { - entity = await this.pipelinesService.create(value); - } + const entity = this.pipeline?.id + ? await this._pipelinesService.update(this.pipeline.id, value) + : await this._pipelinesService.create(value); // Close the dialog with the returned entity - this.dialogRef.close(entity); + this._dialogRef.close(entity); } catch (error) { + // Handle and log any error that occurs during the persistence process console.error(`Error occurred while persisting data: ${error.message}`); + this._errorHandlingService.handleError(error); } } } diff --git a/apps/gauzy/src/app/pages/pipelines/pipelines.component.html b/apps/gauzy/src/app/pages/pipelines/pipelines.component.html index a9e3b26cdeb..302491ca4be 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipelines.component.html +++ b/apps/gauzy/src/app/pages/pipelines/pipelines.component.html @@ -18,18 +18,11 @@

>

- - + + @@ -45,24 +38,15 @@

{{ 'PIPELINES_PAGE.SEARCH_PIPELINE' | translate }} -
+
outline [disabled]="searchForm.invalid" > - {{ - 'PIPELINES_PAGE.SEARCH' - | translate - }} + {{ 'PIPELINES_PAGE.SEARCH' | translate }}
@@ -151,14 +120,10 @@

- + @@ -167,11 +132,7 @@

- +
@@ -221,12 +178,7 @@

- @@ -245,9 +197,7 @@

- +
diff --git a/apps/gauzy/src/app/pages/pipelines/pipelines.component.ts b/apps/gauzy/src/app/pages/pipelines/pipelines.component.ts index 67d30e83ec2..598752ab418 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipelines.component.ts +++ b/apps/gauzy/src/app/pages/pipelines/pipelines.component.ts @@ -49,7 +49,6 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements public viewComponentName: ComponentEnum; public pipeline: IPipeline; public organization: IOrganization; - public name: string; public disableButton: boolean = true; public loading: boolean = false; public pipelineTabsEnum = PipelineTabsEnum; @@ -75,11 +74,11 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements } constructor( + public readonly translateService: TranslateService, private readonly fb: UntypedFormBuilder, private readonly pipelinesService: PipelinesService, private readonly toastrService: ToastrService, private readonly dialogService: NbDialogService, - readonly translateService: TranslateService, private readonly store: Store, private readonly httpClient: HttpClient, private readonly errorHandlingService: ErrorHandlingService, @@ -221,16 +220,18 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements // Configure Smart Table settings this.smartTableSettings = { + actions: false, + selectedRowIndex: -1, + noDataMessage: this.getTranslation('SM_TABLE.NO_DATA.PIPELINE'), pager: { display: false, perPage: pagination ? pagination.itemsPerPage : this.minItemPerPage }, - actions: false, - noDataMessage: this.getTranslation('SM_TABLE.NO_DATA.PIPELINE'), columns: { name: { type: 'string', title: this.getTranslation('SM_TABLE.NAME'), + width: '30%', filter: { type: 'custom', component: InputFilterComponent @@ -242,29 +243,30 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements description: { type: 'string', title: this.getTranslation('SM_TABLE.DESCRIPTION'), + width: '30%', filter: { type: 'custom', component: InputFilterComponent }, - filterFunction: (value) => { + filterFunction: (value: string) => { this.setFilter({ field: 'description', search: value }); } }, stages: { title: this.getTranslation('SM_TABLE.STAGE'), type: 'custom', - filter: false, + width: '30%', + isFilterable: false, renderComponent: StageComponent, componentInitFunction: (instance: StatusBadgeComponent, cell: Cell) => { instance.value = cell.getRawValue(); } }, status: { - filter: false, - editor: false, title: this.getTranslation('SM_TABLE.STATUS'), type: 'custom', - width: '5%', + isFilterable: false, + width: '10%', renderComponent: StatusBadgeComponent, componentInitFunction: (instance: StatusBadgeComponent, cell: Cell) => { instance.value = cell.getRawValue(); @@ -306,8 +308,7 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements this.loading = true; // Extract organization and tenant information - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; + const { id: organizationId, tenantId } = this.organization; // Create a new ServerDataSource for pipelines this.smartTableSource = new ServerDataSource(this.httpClient, { @@ -315,9 +316,7 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements relations: ['stages'], join: { alias: 'pipeline', - leftJoin: { - stages: 'pipeline.stages' - }, + leftJoin: { stages: 'pipeline.stages' }, ...(this.filters.join ? this.filters.join : {}) }, where: { @@ -413,15 +412,17 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements }); } + // Open a dialog to handle manual job application + const dialog = this.dialogService.open(DeleteConfirmationComponent, { + context: { + recordType: this.getTranslation('PIPELINES_PAGE.RECORD_TYPE', this.pipeline) + }, + hasScroll: false + }); + try { - // Open a confirmation dialog and wait for the result - const confirmationResult: 'ok' = await firstValueFrom( - this.dialogService.open(DeleteConfirmationComponent, { - context: { - recordType: this.getTranslation('PIPELINES_PAGE.RECORD_TYPE', this.pipeline) - } - }).onClose - ); + // Wait for dialog result + const confirmationResult = await firstValueFrom(dialog.onClose); // If the user confirms, proceed with deletion if ('ok' === confirmationResult) { @@ -429,17 +430,15 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements await this.pipelinesService.delete(this.pipeline.id); // Display a success message - this.toastrService.success('TOASTR.MESSAGE.PIPELINE_DELETED', { - name: this.pipeline.name - }); - - // Trigger a refresh for the component and pipelines - this._refresh$.next(true); - this.pipelines$.next(true); + this.toastrService.success('TOASTR.MESSAGE.PIPELINE_DELETED', { name: this.pipeline.name }); } } catch (error) { // Handle errors using the error handling service this.errorHandlingService.handleError(error); + } finally { + // Trigger a refresh for the component and pipelines + this._refresh$.next(true); + this.pipelines$.next(true); } } @@ -453,19 +452,22 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements } try { - // Destructure properties needed for creating a pipeline - const { name } = this; - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; + // Open the PipelineFormComponent with the provided context + const dialogRef = this.dialogService.open(PipelineFormComponent); - // Perform the pipeline creation and navigate to the new pipeline - await this.goto({ pipeline: { name, organizationId, tenantId } }); + // Wait for the dialog to close and get the result + const pipeline = await firstValueFrom(dialogRef.onClose); - // Clear the input field after successful pipeline creation - delete this.name; + // If data is received, display a success message and trigger refresh + if (pipeline) { + this.toastrService.success('TOASTR.MESSAGE.PIPELINE_CREATED', { name: pipeline.name }); + } } catch (error) { // Handle errors using the error handling service this.errorHandlingService.handleError(error); + } finally { + this._refresh$.next(true); + this.pipelines$.next(true); } } @@ -488,51 +490,20 @@ export class PipelinesComponent extends PaginationFilterBaseComponent implements return; } - // Destructure properties needed for editing a pipeline - const { name } = this; - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; - // If there is a selected pipeline, update its details if (this.pipeline) { - const { id: pipelineId } = this.pipeline; - - // Perform the pipeline update and navigate to the updated pipeline - await this.goto({ pipeline: { id: pipelineId, name, organizationId, tenantId } }); - - // Clear the input field after successful pipeline update - delete this.name; - } - } catch (error) { - // Handle errors using the error handling service - this.errorHandlingService.handleError(error); - } - } - - /** - * Navigates to the PipelineFormComponent to create or update a pipeline based on the provided context. - * @param context - The context containing pipeline details. - */ - private async goto(context: Record): Promise { - try { - // Open the PipelineFormComponent with the provided context - const dialogRef = this.dialogService.open(PipelineFormComponent, { context }); - - // Wait for the dialog to close and get the result - const data = await firstValueFrom(dialogRef.onClose); - - // Extract pipeline details from the context - const { - pipeline: { id, name } - } = context; + // Open the PipelineFormComponent with the provided context + const dialogRef = this.dialogService.open(PipelineFormComponent, { + context: { pipeline: this.pipeline } + }); - // If data is received, display a success message and trigger refresh - if (data) { - const successMessage = id ? `TOASTR.MESSAGE.PIPELINE_UPDATED` : `TOASTR.MESSAGE.PIPELINE_CREATED`; + // Wait for the dialog to close and get the result + const pipeline = await firstValueFrom(dialogRef.onClose); - this.toastrService.success(successMessage, { - name: id ? name : data.name - }); + // If data is received, display a success message and trigger refresh + if (pipeline) { + this.toastrService.success('TOASTR.MESSAGE.PIPELINE_UPDATED', { name: this.pipeline.name }); + } } } catch (error) { // Handle errors using the error handling service diff --git a/apps/gauzy/src/app/pages/pipelines/pipelines.module.ts b/apps/gauzy/src/app/pages/pipelines/pipelines.module.ts index 4e66e28f9ca..3b4f08be2de 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipelines.module.ts +++ b/apps/gauzy/src/app/pages/pipelines/pipelines.module.ts @@ -29,6 +29,8 @@ import { PipelineDealProbabilityComponent } from './table-components/pipeline-de import { StageComponent } from './stage/stage.component'; import { PipelinesRouting } from './pipelines.routing'; import { PipelinesComponent } from './pipelines.component'; +import { PipelineResolver } from './routes/pipeline.resolver'; +import { DealResolver } from './routes/deal.resolver'; @NgModule({ declarations: [ @@ -49,7 +51,6 @@ import { PipelinesComponent } from './pipelines.component'; PipelinesComponent, StageFormComponent ], - providers: [PipelinesService, DealsService], imports: [ CommonModule, ReactiveFormsModule, @@ -74,6 +75,7 @@ import { PipelinesComponent } from './pipelines.component'; PaginationV2Module, GauzyButtonActionModule, NbTabsetModule - ] + ], + providers: [PipelinesService, DealsService, PipelineResolver, DealResolver] }) export class PipelinesModule {} diff --git a/apps/gauzy/src/app/pages/pipelines/pipelines.routing.ts b/apps/gauzy/src/app/pages/pipelines/pipelines.routing.ts index 0e8d306405e..49a28e5d13c 100644 --- a/apps/gauzy/src/app/pages/pipelines/pipelines.routing.ts +++ b/apps/gauzy/src/app/pages/pipelines/pipelines.routing.ts @@ -1,46 +1,60 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { PermissionsEnum } from '@gauzy/contracts'; +import { PermissionsGuard } from '@gauzy/ui-core/core'; import { PipelinesComponent } from './pipelines.component'; import { PipelineDealsComponent } from './pipeline-deals/pipeline-deals.component'; import { PipelineDealFormComponent } from './pipeline-deals/pipeline-deal-form/pipeline-deal-form.component'; -import { PermissionsGuard } from '@gauzy/ui-core/core'; -import { PermissionsEnum } from '@gauzy/contracts'; - -export function redirectTo() { - return '/pages/dashboard'; -} - -const PIPELINES_VIEW_PERMISSION = { - permissions: { - only: [PermissionsEnum.VIEW_SALES_PIPELINES], - redirectTo - } -}; +import { PipelineResolver } from './routes/pipeline.resolver'; +import { DealResolver } from './routes/deal.resolver'; const routes: Routes = [ { path: '', component: PipelinesComponent, canActivate: [PermissionsGuard], - data: PIPELINES_VIEW_PERMISSION + data: { + permissions: { + only: [PermissionsEnum.VIEW_SALES_PIPELINES], + redirectTo: '/pages/dashboard' + } + } }, { path: ':pipelineId/deals', component: PipelineDealsComponent, canActivate: [PermissionsGuard], - data: PIPELINES_VIEW_PERMISSION + data: { + permissions: { + only: [PermissionsEnum.VIEW_SALES_PIPELINES], + redirectTo: '/pages/dashboard' + } + }, + resolve: { pipeline: PipelineResolver } }, { path: ':pipelineId/deals/create', component: PipelineDealFormComponent, canActivate: [PermissionsGuard], - data: PIPELINES_VIEW_PERMISSION + data: { + permissions: { + only: [PermissionsEnum.VIEW_SALES_PIPELINES], + redirectTo: '/pages/dashboard' + } + }, + resolve: { pipeline: PipelineResolver } }, { path: ':pipelineId/deals/:dealId/edit', component: PipelineDealFormComponent, canActivate: [PermissionsGuard], - data: PIPELINES_VIEW_PERMISSION + data: { + permissions: { + only: [PermissionsEnum.VIEW_SALES_PIPELINES], + redirectTo: '/pages/dashboard' + } + }, + resolve: { pipeline: PipelineResolver, deal: DealResolver } } ]; diff --git a/apps/gauzy/src/app/pages/pipelines/routes/deal.resolver.ts b/apps/gauzy/src/app/pages/pipelines/routes/deal.resolver.ts new file mode 100644 index 00000000000..1c715630108 --- /dev/null +++ b/apps/gauzy/src/app/pages/pipelines/routes/deal.resolver.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { catchError, from, of } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { IDeal } from '@gauzy/contracts'; +import { Store } from '@gauzy/ui-core/common'; +import { DealsService, ErrorHandlingService } from '@gauzy/ui-core/core'; + +@Injectable() +export class DealResolver implements Resolve>> { + constructor( + private readonly _store: Store, + private readonly _dealsService: DealsService, + private readonly _errorHandlingService: ErrorHandlingService + ) {} + + /** + * Resolve method to fetch a deal by its ID. + * + * @param route The activated route snapshot containing the route parameters. + * @returns An observable of IDeal or null if no dealId is present. + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const dealId = route.params['dealId']; + if (!dealId) { + return of(null); + } + + const { id: organizationId, tenantId } = this._store.selectedOrganization; + return from(this._dealsService.getById(dealId, { organizationId, tenantId }, ['client'])).pipe( + catchError((error) => { + // Handle and log errors + this._errorHandlingService.handleError(error); + return of(error); + }) + ); + } +} diff --git a/apps/gauzy/src/app/pages/pipelines/routes/pipeline.resolver.ts b/apps/gauzy/src/app/pages/pipelines/routes/pipeline.resolver.ts new file mode 100644 index 00000000000..c0def4d0450 --- /dev/null +++ b/apps/gauzy/src/app/pages/pipelines/routes/pipeline.resolver.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { Observable } from 'rxjs/internal/Observable'; +import { IPipeline } from '@gauzy/contracts'; +import { ErrorHandlingService, PipelinesService } from '@gauzy/ui-core/core'; +import { Store } from '@gauzy/ui-core/common'; + +@Injectable() +export class PipelineResolver implements Resolve>> { + constructor( + private readonly _store: Store, + private readonly _router: Router, + private readonly _pipelinesService: PipelinesService, + private readonly _errorHandlingService: ErrorHandlingService + ) {} + + /** + * Resolves a pipeline entity by its ID from the route parameters. + * + * @param route - The activated route snapshot containing route information. + * @returns An observable of the pipeline entity. + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const pipelineId = route.params['pipelineId']; + if (!pipelineId) { + return of(null); + } + + const { id: organizationId, tenantId } = this._store.selectedOrganization; + return this._pipelinesService.getById(pipelineId, { organizationId, tenantId }, ['stages']).pipe( + map((pipeline: IPipeline) => { + if (pipeline.organizationId !== organizationId) { + this._router.navigate(['pages/sales/pipelines']); + } + return pipeline; + }), + catchError((error) => { + // Handle and log errors + this._errorHandlingService.handleError(error); + return of(error); + }) + ); + } +} diff --git a/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-created-by/pipeline-deal-created-by.ts b/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-created-by/pipeline-deal-created-by.ts index e7c33f34dbc..abf530c86ee 100644 --- a/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-created-by/pipeline-deal-created-by.ts +++ b/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-created-by/pipeline-deal-created-by.ts @@ -3,8 +3,7 @@ import { IDeal } from '@gauzy/contracts'; @Component({ selector: 'ga-pipeline-deal-created-by', - template: `{{ rowData?.createdBy?.firstName }} - {{ rowData?.createdBy?.lastName }}` + template: `{{ rowData?.createdBy?.name }}` }) export class PipelineDealCreatedByComponent { @Input() diff --git a/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-excerpt/pipeline-deal-excerpt.component.ts b/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-excerpt/pipeline-deal-excerpt.component.ts index 68bacab6f60..9deb74d7aa9 100644 --- a/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-excerpt/pipeline-deal-excerpt.component.ts +++ b/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-excerpt/pipeline-deal-excerpt.component.ts @@ -3,7 +3,7 @@ import { IDeal } from '@gauzy/contracts'; @Component({ selector: 'ga-pipeline-excerpt', - template: `{{ rowData?.stage.name }}` + template: `{{ rowData?.stage?.name }}` }) export class PipelineDealExcerptComponent { @Input() diff --git a/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-probability/pipeline-deal-probability.component.html b/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-probability/pipeline-deal-probability.component.html index c7a519b13b1..123ae85d8c9 100644 --- a/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-probability/pipeline-deal-probability.component.html +++ b/apps/gauzy/src/app/pages/pipelines/table-components/pipeline-deal-probability/pipeline-deal-probability.component.html @@ -1,21 +1,14 @@ -
- {{ probability }} - {{ 'PIPELINE_DEALS_PAGE.LOW' | translate }} -
-
- {{ probability }} - {{ 'PIPELINE_DEALS_PAGE.MEDIUM' | translate }} -
-
- {{ probability }} - {{ 'PIPELINE_DEALS_PAGE.HIGH' | translate }} +
+
{{ probability }} - {{ 'PIPELINE_DEALS_PAGE.LOW' | translate }}
+
{{ probability }} - {{ 'PIPELINE_DEALS_PAGE.LOW' | translate }}
+
+ {{ probability }} - {{ 'PIPELINE_DEALS_PAGE.MEDIUM' | translate }} +
+
+ {{ probability }} - {{ 'PIPELINE_DEALS_PAGE.MEDIUM' | translate }} +
+
{{ probability }} - {{ 'PIPELINE_DEALS_PAGE.HIGH' | translate }}
+
+ {{ probability }} - {{ 'PIPELINE_DEALS_PAGE.UNKNOWN' | translate }} +
diff --git a/apps/gauzy/src/app/pages/projects/projects-routing.module.ts b/apps/gauzy/src/app/pages/projects/projects-routing.module.ts index 14047b1cbf7..a380110fba5 100644 --- a/apps/gauzy/src/app/pages/projects/projects-routing.module.ts +++ b/apps/gauzy/src/app/pages/projects/projects-routing.module.ts @@ -74,7 +74,7 @@ const routes: Routes = [ 'members.user', 'tags', 'teams', - 'repository' + 'customFields.repository' ] } } 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 00818798e8e..4ce0855d895 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 @@ -35,6 +35,7 @@ import { } from '@gauzy/ui-core/shared'; import { ComponentLayoutStyleEnum, + ID, IOrganization, IOrganizationProject, ISelectedEmployee, @@ -73,7 +74,7 @@ export class TaskComponent extends PaginationFilterBaseComponent implements OnIn defaultProject = ALL_PROJECT_SELECTED; taskSubject$: Subject = this.subject$; selectedEmployee: ISelectedEmployee; - selectedEmployeeId: ISelectedEmployee['id']; + selectedEmployeeId: ID; selectedProject: IOrganizationProject; selectedTeamIds: string[] = []; diff --git a/apps/gauzy/src/app/pages/users/users.component.html b/apps/gauzy/src/app/pages/users/users.component.html index 1b630796874..98dcd1c6499 100644 --- a/apps/gauzy/src/app/pages/users/users.component.html +++ b/apps/gauzy/src/app/pages/users/users.component.html @@ -7,12 +7,7 @@

- +
- +
- +
@@ -93,8 +83,7 @@

size="small" underConstruction > - {{ 'BUTTONS.VIEW' | translate }} + {{ 'BUTTONS.VIEW' | translate }} - - diff --git a/apps/gauzy/src/app/pages/users/users.component.ts b/apps/gauzy/src/app/pages/users/users.component.ts index 60e4953cb84..217a0366dfb 100644 --- a/apps/gauzy/src/app/pages/users/users.component.ts +++ b/apps/gauzy/src/app/pages/users/users.component.ts @@ -6,7 +6,7 @@ import { Cell, LocalDataSource } from 'angular2-smart-table'; import { filter, tap } from 'rxjs/operators'; import { debounceTime, firstValueFrom, Subject } from 'rxjs'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { ToastrService, UsersOrganizationsService, monthNames } from '@gauzy/ui-core/core'; +import { ErrorHandlingService, ToastrService, UsersOrganizationsService, monthNames } from '@gauzy/ui-core/core'; import { InvitationTypeEnum, PermissionsEnum, @@ -67,6 +67,7 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI private readonly store: Store, private readonly router: Router, private readonly toastrService: ToastrService, + private readonly errorHandlingService: ErrorHandlingService, private readonly route: ActivatedRoute, public readonly translateService: TranslateService, private readonly userOrganizationsService: UsersOrganizationsService @@ -254,42 +255,82 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI this.showAddCard = false; } - async remove(selectedOrganization: IUserViewModel) { + /** + * Remove user from organization based on user organization ID. + * + * @param selectedOrganization The selected user organization to remove the user from. + */ + async removeUserFromOrganization(selectedOrganization: IUserViewModel) { const { userOrganizationId } = selectedOrganization; const fullName = selectedOrganization.fullName.trim() || selectedOrganization.email; /** * User belongs to only 1 organization -> delete user * User belongs multiple organizations -> remove user from Organization - * */ const count = await this.userOrganizationsService.getUserOrganizationCount(userOrganizationId); const confirmationMessage = count === 1 ? 'FORM.DELETE_CONFIRMATION.DELETE_USER' : 'FORM.DELETE_CONFIRMATION.REMOVE_USER'; - this.dialogService - .open(DeleteConfirmationComponent, { - context: { - recordType: `${fullName} ${this.getTranslation(confirmationMessage)}` - } - }) - .onClose.pipe(untilDestroyed(this)) + // Open a confirmation dialog for the hiring action. + const dialogRef = this.dialogService.open(DeleteConfirmationComponent, { + context: { + recordType: `${fullName} ${this.getTranslation(confirmationMessage)}` + } + }); + + // Open confirmation dialog for user action + dialogRef.onClose + .pipe( + untilDestroyed(this) // Ensures the observable is properly managed to prevent memory leaks. + ) .subscribe(async (result) => { - if (result) { - try { - await this.userOrganizationsService.removeUserFromOrg(userOrganizationId); - this.toastrService.success('USERS_PAGE.REMOVE_USER', { - name: fullName - }); - this._refresh$.next(true); - this.subject$.next(true); - } catch (error) { - this.toastrService.danger(error); - } + if (!result) return; // If the dialog is closed without confirmation, exit the function. + + try { + // If user confirms deletion, proceed with removal from organization + await this.userOrganizationsService.removeUserFromOrg(userOrganizationId); + + this.toastrService.success('USERS_PAGE.REMOVE_USER', { name: fullName }); + } catch (error) { + console.error('Failed to remove user from organization:', error.message); + // Handle errors during the removal process + this.errorHandlingService.handleError(error); + } finally { + // Perform cleanup or refresh actions + this._refresh$.next(true); + this.subject$.next(true); } }); } + /** + * Fetches user organizations with necessary relations. + * + * @returns A promise that resolves to an array of IUserOrganization. + */ + private async _fetchUserOrganizations(): Promise { + // If organization is not available, return undefined + if (!this.organization) { + return; + } + + // Destructure organization properties for readability + const { id: organizationId, tenantId } = this.organization; + + // Fetch user organizations with required relations + const userOrganizations = await this.userOrganizationsService.getAll( + ['user', 'user.role', 'user.tags'], + { organizationId, tenantId }, + true + ); + + // Filter out user organizations that are not active or don't have a user with a role + return userOrganizations.items.filter( + (organization: IUserOrganization) => organization.isActive && organization.user?.role + ); + } + /** * Fetches users from user organizations, maps them to the required format, and loads them into the smart table. */ @@ -300,17 +341,19 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI const organizations = await this._fetchUserOrganizations(); // Mapping fetched organizations to required user format - const users = organizations.map(({ id: userOrganizationId, user, isActive }) => ({ - id: user.id, - fullName: user.name, - email: user.email, - tags: user.tags, - imageUrl: user.imageUrl, - role: user.role, - isActive, - userOrganizationId, - ...this.employeeMapper(user.employee) - })); + const users = organizations + .filter(({ user }) => !!user) + .map(({ id: userOrganizationId, user, isActive }) => ({ + id: user.id, + fullName: user.name, + email: user.email, + tags: user.tags, + imageUrl: user.imageUrl, + role: user.role, + isActive, + userOrganizationId, + ...this.employeeMapper(user.employee) + })); // Initialize Smart Table and load users this.loadUsersToSmartTable(users); @@ -353,31 +396,6 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI }); } - /** - * Fetches user organizations with necessary relations. - * - * @returns A promise that resolves to an array of IUserOrganization. - */ - private async _fetchUserOrganizations(): Promise { - // If organization is not available, return undefined - if (!this.organization) { - return; - } - - // Destructure organization properties for readability - const { id: organizationId, tenantId } = this.organization; - - // Fetch user organizations with required relations - const userOrganizations = await this.userOrganizationsService.getAll( - ['user', 'user.role', 'user.tags'], - { organizationId, tenantId }, - true - ); - - // Filter user organizations based on isActive and user role - return userOrganizations.items.filter((organization) => organization.isActive && organization.user.role); - } - /** * Loads unique user data into the users array if the grid layout is enabled. */ @@ -392,7 +410,7 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI // Filter unique users based on their IDs const uniqueUsers = elements.filter( - (user, index, self) => index === self.findIndex(({ id }) => user.id === id) + (user: IUserOrganization, index: number, self: any) => index === self.findIndex(({ id }) => user.id === id) ); // Add unique users to the users array diff --git a/apps/gauzy/src/index.html b/apps/gauzy/src/index.html index afc1504ccfe..eeac3d6e9a7 100644 --- a/apps/gauzy/src/index.html +++ b/apps/gauzy/src/index.html @@ -88,27 +88,29 @@ animation: spin 1.5s linear infinite; } - +
@@ -117,6 +119,6 @@
- + diff --git a/apps/server-api/src/package.json b/apps/server-api/src/package.json index 4baa57220c8..91372e316f1 100755 --- a/apps/server-api/src/package.json +++ b/apps/server-api/src/package.json @@ -139,15 +139,17 @@ "@datorama/akita": "^7.1.1", "@electron/remote": "^2.0.8", "@gauzy/auth": "^0.1.0", - "@gauzy/changelog-plugin": "^0.1.0", "@gauzy/core": "^0.1.0", "@gauzy/desktop-libs": "^0.1.0", "@gauzy/desktop-window": "^0.1.0", - "@gauzy/jitsu-analytics-plugin": "^0.1.0", - "@gauzy/job-proposal-plugin": "^0.1.0", - "@gauzy/job-search-plugin": "^0.1.0", - "@gauzy/knowledge-base-plugin": "^0.1.0", - "@gauzy/sentry-plugin": "^0.1.0", + "@gauzy/plugin-integration-hubstaff": "^0.1.0", + "@gauzy/plugin-integration-upwork": "^0.1.0", + "@gauzy/plugin-changelog": "^0.1.0", + "@gauzy/plugin-jitsu-analytics": "^0.1.0", + "@gauzy/plugin-job-proposal": "^0.1.0", + "@gauzy/plugin-job-search": "^0.1.0", + "@gauzy/plugin-knowledge-base": "^0.1.0", + "@gauzy/plugin-sentry": "^0.1.0", "@nestjs/platform-express": "^10.3.7", "@sentry/electron": "^4.18.0", "@sentry/node": "^7.101.1", diff --git a/apps/server/src/package.json b/apps/server/src/package.json index ca12f0f3ccb..bd55a53e242 100755 --- a/apps/server/src/package.json +++ b/apps/server/src/package.json @@ -139,15 +139,15 @@ "@datorama/akita": "^7.1.1", "@electron/remote": "^2.0.8", "@gauzy/auth": "^0.1.0", - "@gauzy/changelog-plugin": "^0.1.0", + "@gauzy/plugin-changelog": "^0.1.0", "@gauzy/core": "^0.1.0", "@gauzy/desktop-libs": "^0.1.0", "@gauzy/desktop-window": "^0.1.0", - "@gauzy/jitsu-analytics-plugin": "^0.1.0", - "@gauzy/job-proposal-plugin": "^0.1.0", - "@gauzy/job-search-plugin": "^0.1.0", - "@gauzy/knowledge-base-plugin": "^0.1.0", - "@gauzy/sentry-plugin": "^0.1.0", + "@gauzy/plugin-jitsu-analytics": "^0.1.0", + "@gauzy/plugin-job-proposal": "^0.1.0", + "@gauzy/plugin-job-search": "^0.1.0", + "@gauzy/plugin-knowledge-base": "^0.1.0", + "@gauzy/plugin-sentry": "^0.1.0", "@nestjs/platform-express": "^10.3.7", "@sentry/electron": "^4.18.0", "@sentry/node": "^7.101.1", diff --git a/package.json b/package.json index 35200c3648d..5f832409f55 100644 --- a/package.json +++ b/package.json @@ -137,24 +137,24 @@ "build:package:config:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/config build:prod", "build:package:plugin": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugin build", "build:package:plugin:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugin build", - "build:package:plugins:pre": "yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:integration-jira && yarn run build:package:ui-config && yarn run build:package:ui-core && yarn run build:package:ui-auth && yarn run build:package:plugin:job-search-ui", - "build:package:plugins:pre:prod": "yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:integration-jira:prod && yarn run build:package:ui-config:prod && yarn run build:package:ui-core:prod && yarn run build:package:ui-auth && yarn run build:package:plugin:job-search-ui:prod", - "build:package:plugins:post": "yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog && yarn run build:package:plugin:integration-upwork", - "build:package:plugins:post:prod": "yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:job-proposal:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod && yarn run build:package:plugin:integration-upwork:prod", + "build:package:plugins:pre": "yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:integration-jira && yarn run build:package:ui-config && yarn run build:package:ui-core && yarn run build:package:ui-auth && yarn run build:package:plugin:job-search-ui", + "build:package:plugins:pre:prod": "yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:integration-jira:prod && yarn run build:package:ui-config:prod && yarn run build:package:ui-core:prod && yarn run build:package:ui-auth && yarn run build:package:plugin:job-search-ui:prod", + "build:package:plugins:post": "yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork", + "build:package:plugins:post:prod": "yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:job-proposal:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-upwork:prod", "build:package:plugin:integration-ai": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai build", "build:package:plugin:integration-ai:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai build", "build:package:plugin:integration-hubstaff": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-hubstaff build", - "build:package:plugin:integration-hubstaff:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-hubstaff build", + "build:package:plugin:integration-hubstaff:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-hubstaff build:prod", "build:package:plugin:integration-upwork": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-upwork build", - "build:package:plugin:integration-upwork:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-upwork build", + "build:package:plugin:integration-upwork:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-upwork build:prod", "build:package:plugin:integration-github": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-github build", - "build:package:plugin:integration-github:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-github build", + "build:package:plugin:integration-github:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-github build:prod", "build:package:plugin:integration-jira": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-jira build", "build:package:plugin:integration-jira:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-jira build", "build:package:plugin:sentry": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/sentry-tracing build", "build:package:plugin:sentry:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/sentry-tracing build", "build:package:plugin:jitsu-analytic": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/jitsu-analytics build", - "build:package:plugin:jitsu-analytic:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/jitsu-analytics build", + "build:package:plugin:jitsu-analytic:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/jitsu-analytics build:prod", "build:package:plugin:product-reviews": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/product-reviews build", "build:package:plugin:product-reviews:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/product-reviews build", "build:package:plugin:job-search": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-search build", diff --git a/packages/common/src/custom-embedded-field-types.ts b/packages/common/src/custom-embedded-field-types.ts index 109e562afd3..cb3b5b07647 100644 --- a/packages/common/src/custom-embedded-field-types.ts +++ b/packages/common/src/custom-embedded-field-types.ts @@ -1,45 +1,47 @@ import { RelationOptions as TypeOrmRelationOptions } from 'typeorm'; export type TypeORMInverseSide = string | ((object: T) => any); + +/** + * Extended options for TypeORM relations with additional customization properties. + */ export type TypeORMRelationOptions = Omit & { - /** Indicates whether the relation should be eagerly loaded. Default is false. */ - eager?: boolean; - /** Specifies the cascade options for the relation (e.g., ['insert', 'update', 'remove', 'soft-remove', 'recover']). */ - cascade?: Array<'insert' | 'update' | 'remove' | 'soft-remove' | 'recover'>; - /** Determines whether the relation is nullable. Default is false. */ - nullable?: boolean; - /** Indicates if the relation should have a unique constraint. Default is false. */ - unique?: boolean; - /** Specifies the default value for the relation (for primitive types). */ - default?: any; - /** Specifies if the relation should be persisted automatically. Default is true. */ - persist?: boolean; - /** Additional arbitrary options for the relation (such as indices, etc.). */ - [key: string]: any; + /** Whether the relation should be eagerly loaded. Default is false. */ + eager?: boolean; + /** Cascade options for the relation. */ + cascade?: Array<'insert' | 'update' | 'remove' | 'soft-remove' | 'recover'>; + /** Whether the relation is nullable. Default is false. */ + nullable?: boolean; + /** Whether the relation should have a unique constraint. Default is false. */ + unique?: boolean; + /** Default value for the relation. */ + default?: any; + /** Whether the relation should be persisted automatically. Default is true. */ + persist?: boolean; + /** Additional arbitrary options for the relation (such as indices, etc.). */ + [key: string]: any; }; /** * Configuration for a custom embedded field within a relation. */ -export type RelationCustomEmbeddedFieldConfig = { - /** Name of the custom field. */ - propertyPath: string; - /** Type of the relation field. */ - type: string; - /** Name of the relation. */ - relationType: string; - /** Target entity for the relation. */ - entity: T; - /** A pivot table is an intermediate table that connects two entities in a Many-to-Many relationship. */ - pivotTable?: string; - /** The name of the column in the current entity's table */ - joinColumn?: string; - /** The name of the column in a Many-to-Many relationship */ - inverseJoinColumn?: string; - /** Specifies the inverse side of the relation. */ - inverseSide?: TypeORMInverseSide; - /** Options for the relation */ - options?: TypeORMRelationOptions; +export type RelationCustomEmbeddedFieldConfig = TypeORMRelationOptions & { + /** Name of the custom field. */ + name: string; + /** Type of the relation field. */ + type: string; + /** Name of the relation. */ + relationType?: string; + /** Target entity for the relation. */ + entity?: T; + /** Intermediate table for Many-to-Many relationships. */ + pivotTable?: string; + /** Name of the column in the current entity's table. */ + joinColumn?: string; + /** Name of the column in a Many-to-Many relationship. */ + inverseJoinColumn?: string; + /** Specifies the inverse side of the relation. */ + inverseSide?: TypeORMInverseSide; }; /** @@ -51,14 +53,16 @@ export type CustomEmbeddedFieldConfig = RelationCustomEmbeddedFieldConfig; * Defines custom embedded fields for different entities. */ export interface CustomEmbeddedFields { - /** Custom fields for the Tenant entity. */ - Tenant?: CustomEmbeddedFieldConfig[]; - /** Custom fields for the Organization entity. */ - Organization?: CustomEmbeddedFieldConfig[]; - /** Custom fields for the User entity. */ - User?: CustomEmbeddedFieldConfig[]; - /** Custom fields for the Employee entity. */ - Employee?: CustomEmbeddedFieldConfig[]; - /** Custom fields for the Tag entity. */ - Tag?: CustomEmbeddedFieldConfig[]; + /** Custom fields for the Employee entity. */ + Employee?: CustomEmbeddedFieldConfig[]; + /** Custom fields for the Organization entity. */ + Organization?: CustomEmbeddedFieldConfig[]; + /** Custom fields for the OrganizationProject entity. */ + OrganizationProject?: CustomEmbeddedFieldConfig[]; + /** Custom fields for the Tag entity. */ + Tag?: CustomEmbeddedFieldConfig[]; + /** Custom fields for the Tenant entity. */ + Tenant?: CustomEmbeddedFieldConfig[]; + /** Custom fields for the User entity. */ + User?: CustomEmbeddedFieldConfig[]; } diff --git a/packages/common/src/shared-types.ts b/packages/common/src/shared-types.ts index d41049ab41f..f20aaeffb24 100644 --- a/packages/common/src/shared-types.ts +++ b/packages/common/src/shared-types.ts @@ -23,11 +23,3 @@ export interface Type extends Function { */ new (...args: any[]): T; } - -/** - * Represents an object with custom fields. - * @template T - Type of the custom fields. - */ -export interface CustomFieldsObject { - [key: string]: T; -} diff --git a/packages/config/src/default-configuration.ts b/packages/config/src/default-configuration.ts index 97965ae66ac..a4556c8d573 100644 --- a/packages/config/src/default-configuration.ts +++ b/packages/config/src/default-configuration.ts @@ -66,8 +66,12 @@ export const defaultConfiguration: ApplicationPluginConfig = { }, plugins: [], customFields: { + Employee: [], + Organization: [], + OrganizationProject: [], Tag: [], - Employee: [] + Tenant: [], + User: [] }, authOptions: { expressSessionSecret: process.env.EXPRESS_SESSION_SECRET || 'gauzy', diff --git a/packages/contracts/src/base-entity.model.ts b/packages/contracts/src/base-entity.model.ts index 3ad1fd8140d..1444bb53b32 100644 --- a/packages/contracts/src/base-entity.model.ts +++ b/packages/contracts/src/base-entity.model.ts @@ -33,7 +33,7 @@ export interface IBaseEntityModel extends IBaseSoftDeleteEntityModel { // Common properties for entities associated with a tenant export interface IBasePerTenantEntityModel extends IBaseEntityModel { - tenantId?: ITenant['id']; // Identifier of the associated tenant + tenantId?: ID; // Identifier of the associated tenant tenant?: ITenant; // Reference to the associated tenant } @@ -41,12 +41,12 @@ export interface IBasePerTenantEntityModel extends IBaseEntityModel { export interface IBasePerTenantEntityMutationInput extends Pick, IBaseEntityModel { - tenant?: Pick; + tenant?: Pick & Partial; // Optional fields from ITenant } // Common properties for entities associated with both tenant and organization export interface IBasePerTenantAndOrganizationEntityModel extends IBasePerTenantEntityModel { - organizationId?: IOrganization['id']; // Identifier of the associated organization + organizationId?: ID; // Identifier of the associated organization organization?: IOrganization; // Reference to the associated organization } @@ -54,5 +54,5 @@ export interface IBasePerTenantAndOrganizationEntityModel extends IBasePerTenant export interface IBasePerTenantAndOrganizationEntityMutationInput extends Pick, Partial { - organization?: Pick; + organization?: Pick & Partial; // Allow additional fields from IOrganization } diff --git a/packages/contracts/src/deal.model.ts b/packages/contracts/src/deal.model.ts index 06269c42ef6..4a0d3c1f3c8 100644 --- a/packages/contracts/src/deal.model.ts +++ b/packages/contracts/src/deal.model.ts @@ -1,26 +1,24 @@ -import { IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; +import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; import { IUser } from './user.model'; import { IPipelineStage } from './pipeline-stage.model'; import { IContact } from './contact.model'; export interface IDeal extends IBasePerTenantAndOrganizationEntityModel { - createdByUserId: string; - stageId: string; - clientId?: string; title: string; probability?: number; createdBy: IUser; + createdByUserId: ID; stage: IPipelineStage; + stageId: ID; client?: IContact; + clientId?: ID; } export type IDealFindInput = Partial; -export interface IDealCreateInput - extends IBasePerTenantAndOrganizationEntityModel { - createdByUserId: string; - stageId: string; - clientId?: string; +export interface IDealCreateInput extends IBasePerTenantAndOrganizationEntityModel { + stageId: ID; + clientId?: ID; title: string; probability?: number; } diff --git a/packages/contracts/src/employee.model.ts b/packages/contracts/src/employee.model.ts index 3c4f0a7bf50..0bf95dd7010 100644 --- a/packages/contracts/src/employee.model.ts +++ b/packages/contracts/src/employee.model.ts @@ -1,4 +1,7 @@ -import { IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; +import { + IBasePerTenantAndOrganizationEntityModel, + IBasePerTenantAndOrganizationEntityMutationInput +} from './base-entity.model'; import { IContact } from './contact.model'; import { IEmployeeJobsStatistics } from './employee-job.model'; import { IOrganizationDepartment } from './organization-department.model'; @@ -170,7 +173,7 @@ export interface IEmployeeUpdateInput extends IBasePerTenantAndOrganizationEntit isAway?: boolean; } -export interface IEmployeeCreateInput extends IBasePerTenantAndOrganizationEntityModel { +export interface IEmployeeCreateInput extends IBasePerTenantAndOrganizationEntityMutationInput { user?: IUser; userId?: IUser['id']; password?: string; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 1caaf83b1cd..2195a86393b 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -103,6 +103,7 @@ export * from './role-permission.model'; export * from './role.model'; export * from './screenshot.model'; export * from './seed.model'; +export * from './shared-types'; export * from './skill-entity.model'; export * from './sms.model'; export * from './social-account.model'; diff --git a/packages/contracts/src/integration.model.ts b/packages/contracts/src/integration.model.ts index 5b59d442b29..1bbc0b86448 100644 --- a/packages/contracts/src/integration.model.ts +++ b/packages/contracts/src/integration.model.ts @@ -1,18 +1,25 @@ -import { IBaseEntityModel, IBasePerTenantAndOrganizationEntityModel, IBaseRelationsEntityModel } from './base-entity.model'; +import { + IBaseEntityModel, + IBasePerTenantAndOrganizationEntityModel, + IBaseRelationsEntityModel, + ID +} from './base-entity.model'; import { ITag } from './tag.model'; import { IIntegrationSetting } from './integration-setting.model'; export interface IRelationIntegration { integration?: IIntegration; - integrationId?: IIntegration['id']; + integrationId?: ID; } export interface IRelationalIntegrationTenant { integration?: IIntegrationTenant; - integrationId?: IIntegrationTenant['id']; + integrationId?: ID; } -export interface IIntegrationEntitySetting extends IBasePerTenantAndOrganizationEntityModel, IRelationalIntegrationTenant { +export interface IIntegrationEntitySetting + extends IBasePerTenantAndOrganizationEntityModel, + IRelationalIntegrationTenant { entity: IntegrationEntity; sync: boolean; tiedEntities?: IIntegrationEntitySettingTied[]; @@ -22,7 +29,7 @@ export interface IIntegrationEntitySettingTied extends IBasePerTenantAndOrganiza entity: IntegrationEntity; sync: boolean; integrationEntitySetting?: IIntegrationEntitySetting; - integrationEntitySettingId?: IIntegrationEntitySetting['id']; + integrationEntitySettingId?: ID; } export interface IIntegrationMap extends IBasePerTenantAndOrganizationEntityModel, IRelationalIntegrationTenant { @@ -44,7 +51,10 @@ export interface IIntegrationTenant extends IBasePerTenantAndOrganizationEntityM settings?: IIntegrationSetting[]; } -export interface IIntegrationTenantFindInput extends IBasePerTenantAndOrganizationEntityModel, IBaseRelationsEntityModel, IRelationIntegration { +export interface IIntegrationTenantFindInput + extends IBasePerTenantAndOrganizationEntityModel, + IBaseRelationsEntityModel, + IRelationIntegration { name?: IntegrationEnum; } @@ -81,7 +91,9 @@ export interface IIntegrationFilter { } /** */ -export interface IIntegrationMapSyncBase extends IBasePerTenantAndOrganizationEntityModel, IRelationalIntegrationTenant { +export interface IIntegrationMapSyncBase + extends IBasePerTenantAndOrganizationEntityModel, + IRelationalIntegrationTenant { sourceId?: IIntegrationMap['sourceId']; } @@ -90,7 +102,7 @@ export interface IIntegrationMapSyncEntity extends IIntegrationMapSyncBase { } export interface IIntegrationMapSyncEntityInput extends IIntegrationMapSyncBase { - gauzyId: IIntegrationMap['gauzyId']; + gauzyId: ID; entity: IntegrationEntity; } @@ -101,7 +113,7 @@ export interface IIntegrationTenantCreateInput extends IBasePerTenantAndOrganiza } export interface IIntegrationTenantUpdateInput extends Partial { - id?: IIntegrationTenant['id']; + id?: ID; } export enum IntegrationEnum { @@ -159,8 +171,8 @@ export enum IntegrationFilterEnum { } /** -* Hubstaff Integration -*/ + * Hubstaff Integration + */ export interface IEntitySettingToSync { previousValue: IIntegrationEntitySetting[]; currentValue: IIntegrationEntitySetting[]; diff --git a/packages/contracts/src/organization-projects.model.ts b/packages/contracts/src/organization-projects.model.ts index 2bb0136d63a..d1bf27454fb 100644 --- a/packages/contracts/src/organization-projects.model.ts +++ b/packages/contracts/src/organization-projects.model.ts @@ -1,35 +1,33 @@ import { IEmployee } from './employee.model'; import { IOrganizationContact, IRelationalOrganizationContact } from './organization-contact.model'; -import { - CrudActionEnum, - ProjectBillingEnum, - ProjectOwnerEnum -} from './organization.model'; -import { IBaseEntityWithMembers } from './entity-with-members.model'; +import { CrudActionEnum, ProjectBillingEnum, ProjectOwnerEnum } from './organization.model'; import { ITag } from './tag.model'; import { ITask } from './task.model'; import { IOrganizationSprint } from './organization-sprint.model'; import { IPayment } from './payment.model'; -import { IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; +import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; import { CurrenciesEnum } from './currency.model'; import { ITimeLog } from './timesheet.model'; import { IRelationalImageAsset } from './image-asset.model'; import { IOrganizationTeam } from './organization-team.model'; -import { IOrganizationGithubRepository } from './github.model'; +import { CustomFieldsObject } from './shared-types'; export interface IRelationalOrganizationProject { project?: IOrganizationProject; - projectId?: IOrganizationProject['id']; + projectId?: ID; } export interface IOrganizationProjectSetting extends IBasePerTenantAndOrganizationEntityModel { - repositoryId?: IOrganizationGithubRepository['id']; + customFields?: CustomFieldsObject; isTasksAutoSync?: boolean; isTasksAutoSyncOnLabel?: boolean; syncTag?: string; } -export interface IOrganizationProject extends IBaseEntityWithMembers, IRelationalImageAsset, IRelationalOrganizationContact, IOrganizationProjectSetting { +export interface IOrganizationProject + extends IRelationalImageAsset, + IRelationalOrganizationContact, + IOrganizationProjectSetting { name: string; startDate?: Date; endDate?: Date; @@ -61,9 +59,6 @@ export interface IOrganizationProject extends IBaseEntityWithMembers, IRelationa budgetType?: OrganizationProjectBudgetTypeEnum; membersCount?: number; imageUrl?: string; - /** Project Sync With Repository */ - repository?: IOrganizationGithubRepository; - repositoryId?: IOrganizationGithubRepository['id']; } export enum TaskListTypeEnum { @@ -86,7 +81,9 @@ export interface IOrganizationProjectsFindInput extends IBasePerTenantAndOrganiz billingFlat?: boolean; } -export interface IOrganizationProjectCreateInput extends IBasePerTenantAndOrganizationEntityModel, IRelationalImageAsset { +export interface IOrganizationProjectCreateInput + extends IBasePerTenantAndOrganizationEntityModel, + IRelationalImageAsset { name?: string; organizationContact?: IOrganizationContact; organizationContactId?: IOrganizationContact['id']; @@ -112,7 +109,6 @@ export interface IOrganizationProjectCreateInput extends IBasePerTenantAndOrgani export interface IOrganizationProjectUpdateInput extends IOrganizationProjectCreateInput, IOrganizationProjectSetting { id?: IOrganizationContact['id']; - } export interface IOrganizationProjectStoreState { diff --git a/packages/contracts/src/pipeline.model.ts b/packages/contracts/src/pipeline.model.ts index 5c902263cb8..f754aaa6bc4 100644 --- a/packages/contracts/src/pipeline.model.ts +++ b/packages/contracts/src/pipeline.model.ts @@ -1,8 +1,5 @@ import { IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; -import { - IPipelineStageCreateInput, - IPipelineStage -} from './pipeline-stage.model'; +import { IPipelineStageCreateInput, IPipelineStage } from './pipeline-stage.model'; export interface IPipeline extends IBasePerTenantAndOrganizationEntityModel { stages: IPipelineStage[]; @@ -10,19 +7,15 @@ export interface IPipeline extends IBasePerTenantAndOrganizationEntityModel { name: string; } -export type IPipelineFindInput = Partial< - Pick ->; +export type IPipelineFindInput = Partial>; -export interface IPipelineCreateInput - extends IBasePerTenantAndOrganizationEntityModel { +export interface IPipelineCreateInput extends IBasePerTenantAndOrganizationEntityModel { stages?: IPipelineStageCreateInput[]; description?: string; name: string; - isActive: boolean; } export enum PipelineTabsEnum { - ACTIONS = "ACTIONS", - SEARCH = "SEARCH" -} \ No newline at end of file + ACTIONS = 'ACTIONS', + SEARCH = 'SEARCH' +} diff --git a/packages/contracts/src/shared-types.ts b/packages/contracts/src/shared-types.ts new file mode 100644 index 00000000000..28901cc8b6f --- /dev/null +++ b/packages/contracts/src/shared-types.ts @@ -0,0 +1,7 @@ +/** + * Represents an object with custom fields. + * @template T - Type of the custom fields. + */ +export interface CustomFieldsObject { + [key: string]: T; +} diff --git a/packages/contracts/src/user-organization.model.ts b/packages/contracts/src/user-organization.model.ts index e39b530e270..f86b5f53379 100644 --- a/packages/contracts/src/user-organization.model.ts +++ b/packages/contracts/src/user-organization.model.ts @@ -1,30 +1,19 @@ -import { IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; -import { LanguagesEnum, IUser } from './user.model'; +import { IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IUser } from './user.model'; -export interface IUserOrganization - extends IBasePerTenantAndOrganizationEntityModel { - userId: string; +// Define the base interface for shared properties +export interface IBaseUserOrganization extends IBasePerTenantAndOrganizationEntityModel { + userId?: ID; isDefault: boolean; - user?: IUser; } -export interface IUserOrganizationFindInput - extends IBasePerTenantAndOrganizationEntityModel { - id?: string; - userId?: string; - isDefault?: boolean; - isActive?: boolean; +// Extend the base interface for specific use cases +export interface IUserOrganization extends IBaseUserOrganization { + user?: IUser; } -export interface IUserOrganizationCreateInput - extends IBasePerTenantAndOrganizationEntityModel { - userId: string; - isDefault?: boolean; - isActive?: boolean; -} +// Use the base interface directly for find input +export interface IUserOrganizationFindInput extends Partial {} -export interface IUserOrganizationDeleteInput { - userOrganizationId: string; - requestingUser: IUser; - language?: LanguagesEnum; -} +// Use the base interface directly for create input +export interface IUserOrganizationCreateInput extends IBaseUserOrganization {} diff --git a/packages/contracts/src/user.model.ts b/packages/contracts/src/user.model.ts index caacef523cc..6a60a1967c2 100644 --- a/packages/contracts/src/user.model.ts +++ b/packages/contracts/src/user.model.ts @@ -83,6 +83,7 @@ export interface IUserRegistrationInput { isImporting?: boolean; sourceId?: string; inviteId?: string; + featureAsEmployee?: boolean; } /** diff --git a/packages/core/package.json b/packages/core/package.json index 7ceeee636b0..f8133f0fc28 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -50,14 +50,11 @@ "@gauzy/config": "^0.1.0", "@gauzy/contracts": "^0.1.0", "@gauzy/integration-ai": "^0.1.0", - "@gauzy/integration-github": "^0.1.0", - "@gauzy/integration-hubstaff": "^0.1.0", "@gauzy/integration-jira": "^0.1.0", "@gauzy/plugin": "^0.1.0", "@godaddy/terminus": "^4.12.1", "@grpc/grpc-js": "^1.7.3", "@honeycombio/opentelemetry-node": "0.6.1", - "@jitsu/js": "^1.8.2", "@mikro-orm/better-sqlite": "^6.2.3", "@mikro-orm/core": "^6.2.3", "@mikro-orm/knex": "^6.2.3", @@ -165,6 +162,7 @@ "moment-range": "^4.0.2", "moment-timezone": "^0.5.45", "mqtt": "^4.3.7", + "multer": "1.4.4-lts.1", "multer-s3": "^3.0.1", "multer-storage-cloudinary": "^4.0.0", "mysql2": "^3.9.7", @@ -195,7 +193,6 @@ "underscore": "^1.13.3", "unleash-client": "^3.16.1", "unzipper": "^0.10.11", - "upwork-api": "^1.3.8", "uuid": "^8.3.0", "web-push": "^3.4.4", "yargs": "^17.5.0" diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index 87318b2da1c..c6076193d41 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -4,7 +4,6 @@ import { Module, OnModuleInit } from '@nestjs/common'; import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { MulterModule } from '@nestjs/platform-express'; import { ThrottlerModule } from '@nestjs/throttler'; -import { ThrottlerBehindProxyGuard } from 'throttler/throttler-behind-proxy.guard'; import { ServeStaticModule, ServeStaticModuleOptions } from '@nestjs/serve-static'; import { ClsModule, ClsService } from 'nestjs-cls'; import { HeaderResolver, I18nModule } from 'nestjs-i18n'; @@ -13,8 +12,8 @@ import * as path from 'path'; import * as moment from 'moment'; import { LanguagesEnum } from '@gauzy/contracts'; import { ConfigService, environment } from '@gauzy/config'; -import { ProbotModule } from '@gauzy/integration-github'; import { JiraModule } from '@gauzy/integration-jira'; +import { ThrottlerBehindProxyGuard } from './throttler/throttler-behind-proxy.guard'; import { CoreModule } from './core/core.module'; import { RequestContext } from './core/context/request-context'; import { SharedModule } from './shared/shared.module'; @@ -144,7 +143,7 @@ import { TaskEstimationModule } from './tasks/estimation/task-estimation.module' import { DailyPlanModule } from './tasks/daily-plan/daily-plan.module'; import { SocialAccountModule } from './auth/social-account/social-account.module'; -const { unleashConfig, github, jira } = environment; +const { unleashConfig, jira } = environment; if (unleashConfig.url) { const unleashInstanceConfig: UnleashConfig = { @@ -296,20 +295,6 @@ if (environment.THROTTLE_ENABLED) { }, resolvers: [new HeaderResolver(['language'])] }), - // Probot Configuration - ProbotModule.forRoot({ - isGlobal: true, - // Webhook URL in GitHub will be: https://api.gauzy.co/api/integration/github/webhook - path: 'integration/github/webhook', - config: { - /** Client Configuration */ - clientId: github.clientId, - clientSecret: github.clientSecret, - appId: github.appId, - privateKey: github.appPrivateKey, - webhookSecret: github.webhookSecret - } - }), JiraModule.forRoot({ isGlobal: true, config: { diff --git a/packages/core/src/auth/auth.module.ts b/packages/core/src/auth/auth.module.ts index 65d347fda61..ee587dcb158 100644 --- a/packages/core/src/auth/auth.module.ts +++ b/packages/core/src/auth/auth.module.ts @@ -6,20 +6,21 @@ import { HttpModule } from '@nestjs/axios'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { SocialAuthModule } from '@gauzy/auth'; import { EventBusModule } from '../event-bus/event-bus.module'; -import { Organization, OrganizationTeam, UserOrganization } from './../core/entities/internal'; -import { EmailSendModule } from './../email-send/email-send.module'; +import { OrganizationTeam, UserOrganization } from '../core/entities/internal'; +import { EmailSendModule } from '../email-send/email-send.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { CommandHandlers } from './commands/handlers'; import { JwtRefreshTokenStrategy, JwtStrategy } from './strategies'; import { UserOrganizationService } from '../user-organization/user-organization.services'; -import { UserModule } from './../user/user.module'; -import { EmployeeModule } from './../employee/employee.module'; -import { RoleModule } from './../role/role.module'; -import { PasswordResetModule } from './../password-reset/password-reset.module'; +import { UserModule } from '../user/user.module'; +import { EmployeeModule } from '../employee/employee.module'; +import { RoleModule } from '../role/role.module'; +import { OrganizationModule } from '../organization/organization.module'; +import { PasswordResetModule } from '../password-reset/password-reset.module'; import { EmailConfirmationService } from './email-confirmation.service'; import { EmailVerificationController } from './email-verification.controller'; -import { FeatureModule } from './../feature/feature.module'; +import { FeatureModule } from '../feature/feature.module'; import { SocialAccountService } from './social-account/social-account.service'; import { SocialAccountModule } from './social-account/social-account.module'; @@ -46,6 +47,7 @@ const strategies = [JwtStrategy, JwtRefreshTokenStrategy]; UserModule, EmployeeModule, RoleModule, + OrganizationModule, PasswordResetModule, CqrsModule, SocialAccountModule, @@ -53,12 +55,13 @@ const strategies = [JwtStrategy, JwtRefreshTokenStrategy]; ], useClass: AuthService }), - TypeOrmModule.forFeature([UserOrganization, Organization, OrganizationTeam]), - MikroOrmModule.forFeature([UserOrganization, Organization, OrganizationTeam]), + TypeOrmModule.forFeature([UserOrganization, OrganizationTeam]), + MikroOrmModule.forFeature([UserOrganization, OrganizationTeam]), EmailSendModule, UserModule, EmployeeModule, RoleModule, + OrganizationModule, PasswordResetModule, FeatureModule, CqrsModule, diff --git a/packages/core/src/auth/auth.service.ts b/packages/core/src/auth/auth.service.ts index 521220779ea..f7d23e1b982 100644 --- a/packages/core/src/auth/auth.service.ts +++ b/packages/core/src/auth/auth.service.ts @@ -38,20 +38,20 @@ import { AccountRegistrationEvent } from '../event-bus/events'; import { EventBus } from '../event-bus/event-bus'; import { ALPHA_NUMERIC_CODE_LENGTH, DEMO_PASSWORD_LESS_MAGIC_CODE } from './../constants'; import { EmailService } from './../email-send/email.service'; -import { User } from '../user/user.entity'; import { UserService } from '../user/user.service'; +import { EmployeeService } from '../employee/employee.service'; import { RoleService } from './../role/role.service'; import { UserOrganizationService } from '../user-organization/user-organization.services'; import { ImportRecordUpdateOrCreateCommand } from './../export-import/import-record'; import { PasswordResetCreateCommand, PasswordResetGetCommand } from './../password-reset/commands'; import { RequestContext } from './../core/context'; import { freshTimestamp, generateRandomAlphaNumericCode } from './../core/utils'; -import { OrganizationTeam, Tenant } from './../core/entities/internal'; +import { Employee, OrganizationTeam, Tenant, User } from './../core/entities/internal'; import { EmailConfirmationService } from './email-confirmation.service'; import { prepareSQLQuery as p } from './../database/database.helper'; -import { TypeOrmUserRepository } from './../user/repository/type-orm-user.repository'; +import { TypeOrmUserRepository } from '../user/repository/type-orm-user.repository'; +import { TypeOrmEmployeeRepository } from '../employee/repository/type-orm-employee.repository'; import { TypeOrmOrganizationTeamRepository } from './../organization-team/repository/type-orm-organization-team.repository'; -import { EmployeeService } from '../employee/employee.service'; import { verifyFacebookToken, verifyGithubToken, @@ -64,7 +64,9 @@ import { SocialAccountService } from './social-account/social-account.service'; export class AuthService extends SocialAuthService { constructor( @InjectRepository(User) - private typeOrmUserRepository: TypeOrmUserRepository, + private readonly typeOrmUserRepository: TypeOrmUserRepository, + @InjectRepository(Employee) + private readonly typeOrmEmployeeRepository: TypeOrmEmployeeRepository, @InjectRepository(OrganizationTeam) private readonly typeOrmOrganizationTeamRepository: TypeOrmOrganizationTeamRepository, private readonly emailConfirmationService: EmailConfirmationService, @@ -209,7 +211,7 @@ export class AuthService extends SocialAuthService { * Verify OAuth token when signin with social media from Ever Teams * * @param provider The provider used with user for signin - * @param token The token generated by OAuth provider from Ever Teams frontent + * @param token The token generated by OAuth provider from Ever Teams frontend * @returns A promise resolved by the provider name and the account ID, both decode from the token * @throws A bad request if the provider used by user is not supported */ @@ -331,7 +333,7 @@ export class AuthService extends SocialAuthService { } /** - * This method links a user to an oAuth account when signin/singup with a social media provider + * This method links a user to an oAuth account when signin/signup with a social media provider * * @param input The body request that contains the token to be verified and the provider name * @returns A promise that resolved with an account creation @@ -529,6 +531,8 @@ export class AuthService extends SocialAuthService { languageCode: LanguagesEnum ): Promise { let tenant = input.user.tenant; + const { organizationId } = input; + // 1. If createdById is provided, get the creating user and use their tenant if (input.createdById) { const creatingUser = await this.userService.findOneByIdString(input.createdById, { @@ -540,43 +544,51 @@ export class AuthService extends SocialAuthService { } // 2. Register new user - const userToCreate = this.typeOrmUserRepository.create({ + const entity = this.typeOrmUserRepository.create({ ...input.user, tenant, ...(input.password ? { hash: await this.getPasswordHash(input.password) } : {}) }); - const createdUser = await this.typeOrmUserRepository.save(userToCreate); + let user = await this.typeOrmUserRepository.save(entity); + + // 3. Create employee for specific user + if (input.featureAsEmployee) { + await this.typeOrmEmployeeRepository.save( + this.typeOrmEmployeeRepository.create({ + ...input, + user, + tenantId: tenant.id, + tenant: { id: tenant.id }, + organizationId, + organization: { id: organizationId } + }) + ); + } - // 3. Email is automatically verified after accepting an invitation + // 4. Email is automatically verified after accepting an invitation if (input.inviteId) { - await this.typeOrmUserRepository.update(createdUser.id, { + await this.typeOrmUserRepository.update(user.id, { emailVerifiedAt: freshTimestamp() }); } - // 4. Find the latest registered user with role - const user = await this.typeOrmUserRepository.findOne({ - where: { - id: createdUser.id - }, - relations: { - role: true - } + // 5. Find the latest registered user with role + user = await this.typeOrmUserRepository.findOne({ + where: { id: user.id }, + relations: { role: true } }); - // 5. If organizationId is provided, add the user to the organization + // 6. If organizationId is provided, add the user to the organization if (isNotEmpty(input.organizationId)) { await this.userOrganizationService.addUserToOrganization(user, input.organizationId); } - // 6. Create Import Records while migrating for a relative user - const { isImporting = false, sourceId = null } = input; - if (isImporting && sourceId) { - const { sourceId } = input; + // 7. Create Import Records while migrating for a relative user + if (input.isImporting && input.sourceId) { this.commandBus.execute( new ImportRecordUpdateOrCreateCommand({ entityType: this.typeOrmUserRepository.metadata.tableName, - sourceId, + sourceId: input.sourceId, destinationId: user.id }) ); @@ -593,7 +605,7 @@ export class AuthService extends SocialAuthService { 'companyName' ]); - // 7. If the user's email is not verified, send an email verification + // 8. If the user's email is not verified, send an email verification if (!user.emailVerifiedAt) { this.emailConfirmationService.sendEmailVerification(user, integration); } @@ -603,7 +615,7 @@ export class AuthService extends SocialAuthService { const event = new AccountRegistrationEvent(ctx, user); // ToDo: Send a welcome email to user from events await this.eventBus.publish(event); - // 8. Send a welcome email to the user + // 9. Send a welcome email to the user this.emailService.welcomeUser(input.user, languageCode, input.organizationId, input.originalUrl, integration); return user; } diff --git a/packages/core/src/auth/commands/handlers/auth.login.handler.ts b/packages/core/src/auth/commands/handlers/auth.login.handler.ts index 5c28a5522af..456503f336e 100644 --- a/packages/core/src/auth/commands/handlers/auth.login.handler.ts +++ b/packages/core/src/auth/commands/handlers/auth.login.handler.ts @@ -5,10 +5,7 @@ import { AuthService } from '../../auth.service'; @CommandHandler(AuthLoginCommand) export class AuthLoginHandler implements ICommandHandler { - - constructor( - private readonly authService: AuthService - ) { } + constructor(private readonly authService: AuthService) {} public async execute(command: AuthLoginCommand): Promise { const { input } = command; diff --git a/packages/core/src/auth/commands/handlers/auth.register.handler.ts b/packages/core/src/auth/commands/handlers/auth.register.handler.ts index b2b2509622f..cb434c19f10 100644 --- a/packages/core/src/auth/commands/handlers/auth.register.handler.ts +++ b/packages/core/src/auth/commands/handlers/auth.register.handler.ts @@ -6,33 +6,38 @@ import { AuthService } from '../../auth.service'; import { UserService } from '../../../user/user.service'; @CommandHandler(AuthRegisterCommand) -export class AuthRegisterHandler - implements ICommandHandler { - constructor( - private readonly authService: AuthService, - private readonly userService: UserService - ) { } +export class AuthRegisterHandler implements ICommandHandler { + constructor(private readonly authService: AuthService, private readonly userService: UserService) {} + /** + * Executes the user registration command, handling specific checks for SUPER_ADMIN role. + * + * @param command The AuthRegisterCommand containing user registration input and optional parameters. + * @returns A Promise resolving to the registered IUser object. + * @throws BadRequestException if input is missing required fields. + * @throws UnauthorizedException if the user initiating registration is not authorized. + */ public async execute(command: AuthRegisterCommand): Promise { const { input, languageCode } = command; - if ( - input.user && - input.user.role && - input.user.role.name === RolesEnum.SUPER_ADMIN - ) { + + // Check if the user role is SUPER_ADMIN and require 'createdById' for verification + if (input.user && input.user.role && input.user.role.name === RolesEnum.SUPER_ADMIN) { if (!input.createdById) { - throw new BadRequestException() - }; + throw new BadRequestException('Missing createdById for SUPER_ADMIN registration.'); + } + // Fetch role details of the creator const { role } = await this.userService.findOneByIdString(input.createdById, { - relations: { - role: true - } - }) + relations: { role: true } + }); + + // Verify if the creator's role is SUPER_ADMIN if (role.name !== RolesEnum.SUPER_ADMIN) { - throw new UnauthorizedException(); + throw new UnauthorizedException('Only SUPER_ADMIN can register other SUPER_ADMIN users.'); } } + + // Register the user using the AuthService return await this.authService.register(input, languageCode); } } diff --git a/packages/core/src/core/crud/crud.controller.ts b/packages/core/src/core/crud/crud.controller.ts index 1156f147b01..bba9e29e508 100644 --- a/packages/core/src/core/crud/crud.controller.ts +++ b/packages/core/src/core/crud/crud.controller.ts @@ -2,18 +2,7 @@ // MIT License, see https://github.com/xmlking/ngx-starter-kit/blob/develop/LICENSE // Copyright (c) 2018 Sumanth Chinthagunta -import { - Get, - Post, - Put, - Delete, - Body, - Param, - HttpStatus, - HttpCode, - Query, - UsePipes -} from '@nestjs/common'; +import { Get, Post, Put, Delete, Body, Param, HttpStatus, HttpCode, Query, UsePipes } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { IPagination } from '@gauzy/contracts'; import { DeepPartial, FindOptionsWhere } from 'typeorm'; @@ -21,8 +10,8 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity import { BaseEntity } from '../entities/internal'; import { ICrudService } from './icrud.service'; import { PaginationParams } from './pagination-params'; -import { AbstractValidationPipe, UUIDValidationPipe } from './../../shared/pipes'; -import { TenantOrganizationBaseDTO } from 'core/dto'; +import { AbstractValidationPipe, UUIDValidationPipe } from '../../shared/pipes'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; @ApiResponse({ status: HttpStatus.UNAUTHORIZED, @@ -34,7 +23,7 @@ import { TenantOrganizationBaseDTO } from 'core/dto'; }) @ApiBearerAuth() export abstract class CrudController { - protected constructor(private readonly crudService: ICrudService) { } + protected constructor(private readonly crudService: ICrudService) {} /** * Get the total count of all records. @@ -47,12 +36,10 @@ export abstract class CrudController { @ApiOperation({ summary: 'Get total record count' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Total record count retrieved successfully', + description: 'Total record count retrieved successfully' }) @Get('count') - async getCount( - @Query() options?: FindOptionsWhere, - ): Promise { + async getCount(@Query() options?: FindOptionsWhere): Promise { return await this.crudService.countBy(options); } @@ -67,13 +54,10 @@ export abstract class CrudController { @ApiOperation({ summary: 'Get paginated records' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Records retrieved successfully', + description: 'Records retrieved successfully' }) @Get('pagination') - async pagination( - @Query() filter?: PaginationParams, - ...options: any[] - ): Promise | void> { + async pagination(@Query() filter?: PaginationParams, ...options: any[]): Promise | void> { return this.crudService.paginate(filter); } @@ -88,13 +72,10 @@ export abstract class CrudController { @ApiOperation({ summary: 'Get all records' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Records retrieved successfully', + description: 'Records retrieved successfully' }) @Get() - async findAll( - filter?: PaginationParams, - ...options: any[] - ): Promise> { + async findAll(filter?: PaginationParams, ...options: any[]): Promise> { return this.crudService.findAll(filter); } @@ -109,17 +90,14 @@ export abstract class CrudController { @ApiOperation({ summary: 'Find record by ID' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Record retrieved successfully', + description: 'Record retrieved successfully' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Record not found', + description: 'Record not found' }) @Get(':id') - async findById( - @Param('id', UUIDValidationPipe) id: T['id'], - ...options: any[] - ): Promise { + async findById(@Param('id', UUIDValidationPipe) id: T['id'], ...options: any[]): Promise { return this.crudService.findOneByIdString(id); } @@ -134,18 +112,15 @@ export abstract class CrudController { @ApiOperation({ summary: 'Create new record' }) @ApiResponse({ status: HttpStatus.CREATED, - description: 'Record created successfully', + description: 'Record created successfully' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input provided', + description: 'Invalid input provided' }) @HttpCode(HttpStatus.CREATED) @Post() - async create( - @Body() entity: DeepPartial, - ...options: any[] - ): Promise { + async create(@Body() entity: DeepPartial, ...options: any[]): Promise { return this.crudService.create(entity); } @@ -161,15 +136,15 @@ export abstract class CrudController { @ApiOperation({ summary: 'Update existing record' }) @ApiResponse({ status: HttpStatus.CREATED, - description: 'Record updated successfully', + description: 'Record updated successfully' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Record not found', + description: 'Record not found' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input provided for update', + description: 'Invalid input provided for update' }) @HttpCode(HttpStatus.ACCEPTED) @Put(':id') @@ -192,18 +167,15 @@ export abstract class CrudController { @ApiOperation({ summary: 'Delete record' }) @ApiResponse({ status: HttpStatus.NO_CONTENT, - description: 'Record deleted successfully', + description: 'Record deleted successfully' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Record not found', + description: 'Record not found' }) @HttpCode(HttpStatus.ACCEPTED) @Delete(':id') - async delete( - @Param('id', UUIDValidationPipe) id: string, - ...options: any[] - ): Promise { + async delete(@Param('id', UUIDValidationPipe) id: string, ...options: any[]): Promise { return this.crudService.delete(id); } @@ -219,19 +191,16 @@ export abstract class CrudController { @ApiOperation({ summary: 'Soft delete a record by ID' }) @ApiResponse({ status: HttpStatus.ACCEPTED, - description: 'Record soft deleted successfully', + description: 'Record soft deleted successfully' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Record not found', + description: 'Record not found' }) @Delete(':id/soft') @HttpCode(HttpStatus.ACCEPTED) @UsePipes(new AbstractValidationPipe({ whitelist: true }, { query: TenantOrganizationBaseDTO })) - async softRemove( - @Param('id', UUIDValidationPipe) id: T['id'], - ...options: any[] - ): Promise { + async softRemove(@Param('id', UUIDValidationPipe) id: T['id'], ...options: any[]): Promise { // Soft delete the record return await this.crudService.softRemove(id, options); } @@ -248,19 +217,16 @@ export abstract class CrudController { @ApiOperation({ summary: 'Restore a soft-deleted record by ID' }) @ApiResponse({ status: HttpStatus.ACCEPTED, - description: 'Record restored successfully', + description: 'Record restored successfully' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Record not found or not in a soft-deleted state', + description: 'Record not found or not in a soft-deleted state' }) @Put(':id/recover') @HttpCode(HttpStatus.ACCEPTED) @UsePipes(new AbstractValidationPipe({ whitelist: true }, { query: TenantOrganizationBaseDTO })) - async softRecover( - @Param('id', UUIDValidationPipe) id: T['id'], - ...options: any[] - ): Promise { + async softRecover(@Param('id', UUIDValidationPipe) id: T['id'], ...options: any[]): Promise { // Restore the soft-deleted record return await this.crudService.softRecover(id, options); } diff --git a/packages/core/src/core/crud/pagination-params.ts b/packages/core/src/core/crud/pagination-params.ts index 8a782de09da..a6ff0903f58 100644 --- a/packages/core/src/core/crud/pagination-params.ts +++ b/packages/core/src/core/crud/pagination-params.ts @@ -14,7 +14,6 @@ import { SimpleObjectLiteral, convertNativeParameters, parseObject } from './pag * Specifies what columns should be retrieved. */ export class OptionsSelect { - @ApiPropertyOptional({ type: 'object' }) @IsOptional() @Transform(({ value }: TransformFnParams) => parseObject(value, parseToBoolean)) @@ -23,9 +22,8 @@ export class OptionsSelect { /** * Indicates what relations of entity should be loaded (simplified left join form). -*/ + */ export class OptionsRelations extends OptionsSelect { - @ApiPropertyOptional({ type: 'object' }) @IsOptional() readonly relations?: FindOptionsRelations; @@ -46,15 +44,15 @@ export class OptionParams extends OptionsRelations { @IsNotEmpty() @ValidateNested({ each: true }) @Type(() => TenantOrganizationBaseDTO) - @Transform(({ value }: TransformFnParams) => value ? escapeQueryWithParameters(value) : {}) + @Transform(({ value }: TransformFnParams) => (value ? escapeQueryWithParameters(value) : {})) readonly where: FindOptionsWhere; /** - * Indicates if soft-deleted rows should be included in entity result. - */ + * Indicates if soft-deleted rows should be included in entity result. + */ @ApiPropertyOptional({ type: 'boolean' }) @IsOptional() - @Transform(({ value }: TransformFnParams) => value ? parseToBoolean(value) : false) + @Transform(({ value }: TransformFnParams) => (value ? parseToBoolean(value) : false)) readonly withDeleted: boolean; } @@ -88,7 +86,6 @@ export class PaginationParams extends OptionParams { * @returns {TenantOrganizationBaseDTO} - The escaped and converted query parameters as a DTO instance. */ export function escapeQueryWithParameters(nativeParameters: SimpleObjectLiteral): TenantOrganizationBaseDTO { - // Convert native parameters based on the database connection type const builtParameters: SimpleObjectLiteral = convertNativeParameters(nativeParameters); diff --git a/packages/core/src/core/decorators/entity/column.helper.ts b/packages/core/src/core/decorators/entity/column.helper.ts index 79bbca93cc7..bff9104690a 100644 --- a/packages/core/src/core/decorators/entity/column.helper.ts +++ b/packages/core/src/core/decorators/entity/column.helper.ts @@ -1,4 +1,5 @@ -import { ColumnDataType, MikroORMColumnOptions } from "./column-options.types"; +import { DataSourceOptions } from 'typeorm'; +import { ColumnDataType, MikroORMColumnOptions } from './column-options.types'; /** * Resolve the database column type. @@ -6,7 +7,22 @@ import { ColumnDataType, MikroORMColumnOptions } from "./column-options.types"; * @returns The resolved column type. */ export function resolveDbType(columnType: ColumnDataType): ColumnDataType { - return columnType; + return columnType; +} + +/** + * Maps a generic type to a database-specific column type based on the provided database engine. + * + * @param dbEngine - The type of the database engine. + * @param type - The generic type to be mapped. + * @returns The database-specific column type. + */ +export function getColumnType(dbEngine: DataSourceOptions['type'], type: string): ColumnDataType { + switch (type) { + case 'string': + return 'varchar'; + } + return 'varchar'; } /** @@ -15,14 +31,14 @@ export function resolveDbType(columnType: ColumnDataType): ColumnDataType { * @returns MikroORM column options. */ export function parseMikroOrmColumnOptions({ type, options }): MikroORMColumnOptions { - if (typeof options?.default === 'function') { - options.default = options.default(); - } - if (options?.relationId) { - options.persist = false; - } - return { - type: type, - ...options - } + if (typeof options?.default === 'function') { + options.default = options.default(); + } + if (options?.relationId) { + options.persist = false; + } + return { + type: type, + ...options + }; } diff --git a/packages/core/src/core/decorators/entity/relations/many-to-one.decorator.ts b/packages/core/src/core/decorators/entity/relations/many-to-one.decorator.ts index 9938de887cb..fb410b4722a 100644 --- a/packages/core/src/core/decorators/entity/relations/many-to-one.decorator.ts +++ b/packages/core/src/core/decorators/entity/relations/many-to-one.decorator.ts @@ -1,10 +1,16 @@ -import { Cascade, EntityName, ManyToOneOptions } from "@mikro-orm/core"; -import { omit } from "underscore"; -import { deepClone } from "@gauzy/common"; -import { ObjectUtils } from "../../../../core/util/object-utils"; -import { TypeOrmManyToOne } from "./type-orm"; -import { MikroOrmManyToOne } from "./mikro-orm"; -import { MikroORMInverseSide, TypeORMInverseSide, TypeORMRelationOptions, TypeORMTarget, TypeOrmCascadeOption } from "./shared-types"; +import { Cascade, EntityName, ManyToOneOptions } from '@mikro-orm/core'; +import { omit } from 'underscore'; +import { deepClone } from '@gauzy/common'; +import { ObjectUtils } from '../../../../core/util/object-utils'; +import { TypeOrmManyToOne } from './type-orm'; +import { MikroOrmManyToOne } from './mikro-orm'; +import { + MikroORMInverseSide, + TypeORMInverseSide, + TypeORMRelationOptions, + TypeORMTarget, + TypeOrmCascadeOption +} from './shared-types'; /** * Options for mapping ManyToOne relationship arguments for MikroORM. @@ -13,16 +19,16 @@ import { MikroORMInverseSide, TypeORMInverseSide, TypeORMRelationOptions, TypeOR * @template O - The type of additional options. */ export interface MapManyToOneArgsForMikroORMOptions { - // The target entity class or function returning the target entity class. - typeFunctionOrTarget: TargetEntity; - // The inverse side of the relationship or additional options if provided. - inverseSideOrOptions?: InverseSide; - // The options for the ManyToOne relationship. - options?: RelationOptions; - // The property key of the target entity. - propertyKey?: string; - // The target string (optional). - target?: string; + // The target entity class or function returning the target entity class. + typeFunctionOrTarget: TargetEntity; + // The inverse side of the relationship or additional options if provided. + inverseSideOrOptions?: InverseSide; + // The options for the ManyToOne relationship. + options?: RelationOptions; + // The property key of the target entity. + propertyKey?: string; + // The target string (optional). + target?: string; } type MikroORMTarget = ManyToOneOptions | string | ((e?: any) => EntityName); @@ -30,9 +36,10 @@ type MikroORMRelationOptions = Omit>, 'casc type TargetEntity = TypeORMTarget | MikroORMTarget; type InverseSide = TypeORMInverseSide & MikroORMInverseSide; -type RelationOptions = MikroORMRelationOptions & TypeORMRelationOptions & { - cascade?: Cascade[] | TypeOrmCascadeOption; -}; +export type RelationOptions = MikroORMRelationOptions & + TypeORMRelationOptions & { + cascade?: Cascade[] | TypeOrmCascadeOption; + }; /** * Decorator for defining Many-to-One relationships in both TypeORM and MikroORM. @@ -43,29 +50,41 @@ type RelationOptions = MikroORMRelationOptions & TypeORMRelationOpti * @returns PropertyDecorator */ export function MultiORMManyToOne( - typeFunctionOrTarget: TargetEntity, - inverseSideOrOptions?: InverseSide | RelationOptions, - options?: RelationOptions + typeFunctionOrTarget: TargetEntity, + inverseSideOrOptions?: InverseSide | RelationOptions, + options?: RelationOptions ): PropertyDecorator { - // Normalize parameters. - let inverseSideProperty: InverseSide; - - if (ObjectUtils.isObject(inverseSideOrOptions)) { - options = >inverseSideOrOptions; - } else { - inverseSideProperty = inverseSideOrOptions as any; - } - - return (target: any, propertyKey: string) => { - // If options are not provided, initialize an empty object - if (!options) options = {} as RelationOptions; - - // Use TypeORM decorator for Many-to-One - TypeOrmManyToOne(typeFunctionOrTarget as TypeORMTarget, inverseSideOrOptions as TypeORMInverseSide, options as TypeORMRelationOptions)(target, propertyKey); - - // Use MikroORM decorator for Many-to-One - MikroOrmManyToOne(mapManyToOneArgsForMikroORM({ typeFunctionOrTarget, inverseSideOrOptions: inverseSideProperty as InverseSide, options, propertyKey, target }))(target, propertyKey); - }; + // Normalize parameters. + let inverseSideProperty: InverseSide; + + if (ObjectUtils.isObject(inverseSideOrOptions)) { + options = >inverseSideOrOptions; + } else { + inverseSideProperty = inverseSideOrOptions as any; + } + + return (target: any, propertyKey: string) => { + // If options are not provided, initialize an empty object + if (!options) options = {} as RelationOptions; + + // Use TypeORM decorator for Many-to-One + TypeOrmManyToOne( + typeFunctionOrTarget as TypeORMTarget, + inverseSideOrOptions as TypeORMInverseSide, + options as TypeORMRelationOptions + )(target, propertyKey); + + // Use MikroORM decorator for Many-to-One + MikroOrmManyToOne( + mapManyToOneArgsForMikroORM({ + typeFunctionOrTarget, + inverseSideOrOptions: inverseSideProperty as InverseSide, + options, + propertyKey, + target + }) + )(target, propertyKey); + }; } /** @@ -74,51 +93,55 @@ export function MultiORMManyToOne( * @param param0 - Destructured parameters object. * @returns MikroORMRelationOptions - The mapped MikroORM relation options. */ -export function mapManyToOneArgsForMikroORM({ typeFunctionOrTarget, options, propertyKey }: MapManyToOneArgsForMikroORMOptions) { - // Cast options to RelationOptions - const typeOrmOptions = deepClone(options) as RelationOptions; - - // Initialize an array to store MikroORM cascade options - let mikroORMCascade: Cascade[] = []; - - // Check if TypeORM cascade options are provided - if (typeOrmOptions?.cascade) { - // Handle boolean cascade option - if (typeof typeOrmOptions.cascade === 'boolean') { - mikroORMCascade = typeOrmOptions.cascade ? [Cascade.ALL] : []; - } - - // Handle array cascade options - if (typeOrmOptions?.cascade instanceof Array) { - // Define a mapping from TypeORM cascade options to MikroORM cascade options - const cascading: { [key: string]: Cascade | null } = { - 'insert': Cascade.PERSIST, - 'update': Cascade.MERGE, - 'remove': Cascade.REMOVE, - 'soft-remove': null, - 'recover': null, - }; - - mikroORMCascade = typeOrmOptions.cascade.map((c: any) => cascading[c] || null).filter(Boolean) as Cascade[]; - } - } - - // Create MikroORM relation options - const mikroOrmOptions: Partial> = { - ...omit(options, 'onDelete', 'onUpdate') as any, - entity: typeFunctionOrTarget as (string | ((e?: any) => EntityName)), - ...(mikroORMCascade.length ? { cascade: mikroORMCascade } : {}), - ...(typeOrmOptions?.onDelete ? { deleteRule: typeOrmOptions?.onDelete?.toLocaleLowerCase() } : {}), - ...(typeOrmOptions?.onUpdate ? { updateRule: typeOrmOptions?.onUpdate?.toLocaleLowerCase() } : {}), - }; - - // Set default joinColumn and referenceColumnName if not provided - if (!mikroOrmOptions.joinColumn && propertyKey) { - // Set default joinColumn if not overwrite in options - mikroOrmOptions.joinColumn = `${propertyKey}Id`; - mikroOrmOptions.referenceColumnName = `id`; - } - - // Return the mapped MikroORM relation options - return mikroOrmOptions as MikroORMRelationOptions +export function mapManyToOneArgsForMikroORM({ + typeFunctionOrTarget, + options, + propertyKey +}: MapManyToOneArgsForMikroORMOptions) { + // Cast options to RelationOptions + const typeOrmOptions = deepClone(options) as RelationOptions; + + // Initialize an array to store MikroORM cascade options + let mikroORMCascade: Cascade[] = []; + + // Check if TypeORM cascade options are provided + if (typeOrmOptions?.cascade) { + // Handle boolean cascade option + if (typeof typeOrmOptions.cascade === 'boolean') { + mikroORMCascade = typeOrmOptions.cascade ? [Cascade.ALL] : []; + } + + // Handle array cascade options + if (typeOrmOptions?.cascade instanceof Array) { + // Define a mapping from TypeORM cascade options to MikroORM cascade options + const cascading: { [key: string]: Cascade | null } = { + insert: Cascade.PERSIST, + update: Cascade.MERGE, + remove: Cascade.REMOVE, + 'soft-remove': null, + recover: null + }; + + mikroORMCascade = typeOrmOptions.cascade.map((c: any) => cascading[c] || null).filter(Boolean) as Cascade[]; + } + } + + // Create MikroORM relation options + const mikroOrmOptions: Partial> = { + ...(omit(options, 'onDelete', 'onUpdate') as any), + entity: typeFunctionOrTarget as string | ((e?: any) => EntityName), + ...(mikroORMCascade.length ? { cascade: mikroORMCascade } : {}), + ...(typeOrmOptions?.onDelete ? { deleteRule: typeOrmOptions?.onDelete?.toLocaleLowerCase() } : {}), + ...(typeOrmOptions?.onUpdate ? { updateRule: typeOrmOptions?.onUpdate?.toLocaleLowerCase() } : {}) + }; + + // Set default joinColumn and referenceColumnName if not provided + if (!mikroOrmOptions.joinColumn && propertyKey) { + // Set default joinColumn if not overwrite in options + mikroOrmOptions.joinColumn = `${propertyKey}Id`; + mikroOrmOptions.referenceColumnName = `id`; + } + + // Return the mapped MikroORM relation options + return mikroOrmOptions as MikroORMRelationOptions; } diff --git a/packages/core/src/core/entities/custom-entity-fields/custom-entity-fields.ts b/packages/core/src/core/entities/custom-entity-fields/custom-entity-fields.ts index 8a484218070..5f13a43a9c1 100644 --- a/packages/core/src/core/entities/custom-entity-fields/custom-entity-fields.ts +++ b/packages/core/src/core/entities/custom-entity-fields/custom-entity-fields.ts @@ -1,13 +1,17 @@ -import { CustomEmbeddedFields } from "@gauzy/common"; -import { MikroOrmEmployeeEntityCustomFields, TypeOrmEmployeeEntityCustomFields } from "./employee"; -import { MikroOrmTagEntityCustomFields, TypeOrmTagEntityCustomFields } from "./tag"; +import { CustomEmbeddedFields } from '@gauzy/common'; +import { MikroOrmEmployeeEntityCustomFields, TypeOrmEmployeeEntityCustomFields } from './employee'; +import { MikroOrmTagEntityCustomFields, TypeOrmTagEntityCustomFields } from './tag'; +import { + MikroOrmOrganizationProjectEntityCustomFields, + TypeOrmOrganizationProjectEntityCustomFields +} from './organization-project'; /** * Defines the structure for entity field registration configuration. */ export type EntityFieldRegistrationConfig = { - entityName: keyof CustomEmbeddedFields; // Entity name from CustomEmbeddedFields - customFields: any; // Custom fields associated with the entity + entityName: keyof CustomEmbeddedFields; // Entity name from CustomEmbeddedFields + customFields: any; // Custom fields associated with the entity }; /** @@ -17,8 +21,9 @@ export type EntityFieldRegistrationConfig = { * Each entry specifies the name of the entity and the associated custom fields. */ export const typeOrmCustomEntityFieldRegistrations: EntityFieldRegistrationConfig[] = [ - { entityName: 'Employee', customFields: TypeOrmEmployeeEntityCustomFields }, - { entityName: 'Tag', customFields: TypeOrmTagEntityCustomFields }, + { entityName: 'Employee', customFields: TypeOrmEmployeeEntityCustomFields }, + { entityName: 'Tag', customFields: TypeOrmTagEntityCustomFields }, + { entityName: 'OrganizationProject', customFields: TypeOrmOrganizationProjectEntityCustomFields } ]; /** @@ -28,6 +33,7 @@ export const typeOrmCustomEntityFieldRegistrations: EntityFieldRegistrationConfi * Each entry specifies the entity name and the corresponding custom fields. */ export const mikroOrmCustomEntityFieldRegistrations: EntityFieldRegistrationConfig[] = [ - { entityName: 'Employee', customFields: MikroOrmEmployeeEntityCustomFields }, - { entityName: 'Tag', customFields: MikroOrmTagEntityCustomFields }, + { entityName: 'Employee', customFields: MikroOrmEmployeeEntityCustomFields }, + { entityName: 'Tag', customFields: MikroOrmTagEntityCustomFields }, + { entityName: 'OrganizationProject', customFields: MikroOrmOrganizationProjectEntityCustomFields } ]; diff --git a/packages/core/src/core/entities/custom-entity-fields/custom-field-types.ts b/packages/core/src/core/entities/custom-entity-fields/custom-field-types.ts index ba963124810..9e4ffc83b24 100644 --- a/packages/core/src/core/entities/custom-entity-fields/custom-field-types.ts +++ b/packages/core/src/core/entities/custom-entity-fields/custom-field-types.ts @@ -1,9 +1,9 @@ -import { CustomFieldsObject } from '@gauzy/common'; +import { CustomFieldsObject } from '@gauzy/contracts'; /** * This interface should be implemented by any entity which can be extended * with custom fields. */ export interface HasCustomFields { - customFields?: CustomFieldsObject; + customFields?: CustomFieldsObject; } diff --git a/packages/core/src/core/entities/custom-entity-fields/mikro-orm-base-custom-entity-field.ts b/packages/core/src/core/entities/custom-entity-fields/mikro-orm-base-custom-entity-field.ts index 2ca9cddc4ba..e5fac49d058 100644 --- a/packages/core/src/core/entities/custom-entity-fields/mikro-orm-base-custom-entity-field.ts +++ b/packages/core/src/core/entities/custom-entity-fields/mikro-orm-base-custom-entity-field.ts @@ -5,7 +5,7 @@ export const __FIX_RELATIONAL_CUSTOM_FIELDS__ = 'fix_relational_custom_fields'; // Define a new entity that extends the abstract base class export abstract class MikroOrmBaseCustomEntityFields { /** - * If there are only relations are defined for an Entity for customFields, then TypeORM not saving realtions for entity ("Cannot set properties of undefined ()"). + * If there are only relations are defined for an Entity for customFields, then TypeORM not saving relations for entity ("Cannot set properties of undefined ()"). * So we have to add a "fake" column to the customFields embedded type to prevent this error from occurring. */ @Property({ type: 'boolean', nullable: true, hidden: true }) diff --git a/packages/core/src/core/entities/custom-entity-fields/organization-project/index.ts b/packages/core/src/core/entities/custom-entity-fields/organization-project/index.ts new file mode 100644 index 00000000000..da7242be82b --- /dev/null +++ b/packages/core/src/core/entities/custom-entity-fields/organization-project/index.ts @@ -0,0 +1,10 @@ +import { TypeOrmOrganizationProjectEntityCustomFields } from './type-orm-organization-project-entity-custom-fields'; +import { MikroOrmOrganizationProjectEntityCustomFields } from './mikro-orm-organization-project-entity-custom-fields'; + +export * from './mikro-orm-organization-project-entity-custom-fields'; +export * from './type-orm-organization-project-entity-custom-fields'; + +// Union type representing either TypeORM or MikroORM custom fields +export type OrganizationProjectEntityCustomFields = + | TypeOrmOrganizationProjectEntityCustomFields + | MikroOrmOrganizationProjectEntityCustomFields; diff --git a/packages/core/src/core/entities/custom-entity-fields/organization-project/mikro-orm-organization-project-entity-custom-fields.ts b/packages/core/src/core/entities/custom-entity-fields/organization-project/mikro-orm-organization-project-entity-custom-fields.ts new file mode 100644 index 00000000000..0a52e75b908 --- /dev/null +++ b/packages/core/src/core/entities/custom-entity-fields/organization-project/mikro-orm-organization-project-entity-custom-fields.ts @@ -0,0 +1,5 @@ +import { Embeddable } from '@mikro-orm/core'; +import { MikroOrmBaseCustomEntityFields } from '../mikro-orm-base-custom-entity-field'; + +@Embeddable() +export class MikroOrmOrganizationProjectEntityCustomFields extends MikroOrmBaseCustomEntityFields {} diff --git a/packages/core/src/core/entities/custom-entity-fields/organization-project/type-orm-organization-project-entity-custom-fields.ts b/packages/core/src/core/entities/custom-entity-fields/organization-project/type-orm-organization-project-entity-custom-fields.ts new file mode 100644 index 00000000000..ad8b247a079 --- /dev/null +++ b/packages/core/src/core/entities/custom-entity-fields/organization-project/type-orm-organization-project-entity-custom-fields.ts @@ -0,0 +1 @@ +export class TypeOrmOrganizationProjectEntityCustomFields {} diff --git a/packages/core/src/core/entities/custom-entity-fields/register-custom-entity-fields.ts b/packages/core/src/core/entities/custom-entity-fields/register-custom-entity-fields.ts index d8b7f3d81fd..cd44f56878a 100644 --- a/packages/core/src/core/entities/custom-entity-fields/register-custom-entity-fields.ts +++ b/packages/core/src/core/entities/custom-entity-fields/register-custom-entity-fields.ts @@ -1,10 +1,48 @@ -import { JoinColumn, JoinTable } from 'typeorm'; +import { Column, JoinColumn, JoinTable, RelationId } from 'typeorm'; import { ApplicationPluginConfig, CustomEmbeddedFields, RelationCustomEmbeddedFieldConfig } from '@gauzy/common'; -import { MultiORMColumn, MultiORMManyToMany, MultiORMManyToOne } from '../../../core/decorators'; +import { getColumnType } from '../../../core/decorators/entity/column.helper'; +import { ColumnIndex, MultiORMColumn, MultiORMManyToMany, MultiORMManyToOne } from '../../../core/decorators'; import { ColumnDataType, ColumnOptions } from '../../../core/decorators/entity/column-options.types'; +import { getDBType } from '../../../core/utils'; import { mikroOrmCustomEntityFieldRegistrations, typeOrmCustomEntityFieldRegistrations } from './custom-entity-fields'; import { __FIX_RELATIONAL_CUSTOM_FIELDS__ } from './mikro-orm-base-custom-entity-field'; +/** + * Defines a column with or without a relation ID and optional indexing. + * + * @param customField - Configuration for the custom field. + * @param name - The name of the field. + * @param instance - The instance of the class where the field is defined. + */ +const defineColumn = ( + config: ApplicationPluginConfig, + customField: RelationCustomEmbeddedFieldConfig, + name: string, + instance: any +) => { + const { nullable, relationId, index = false } = customField; + + // Get the database type from the connection options + let dbEngine = getDBType(config.dbConnectionOptions); + + const options: ColumnDataType | ColumnOptions = { + type: getColumnType(dbEngine, customField.type), + name, + nullable: nullable === false ? false : true, + unique: customField.unique ?? false + }; + + if (relationId) { + RelationId((it: any) => it.customFields[customField.relation])(instance, name); + } + + if (index) { + ColumnIndex()(instance, name); + } + + Column(options)(instance, name); +}; + /** * Registers a custom field for an entity based on the custom field configuration. * @@ -13,35 +51,38 @@ import { __FIX_RELATIONAL_CUSTOM_FIELDS__ } from './mikro-orm-base-custom-entity * @param instance - The entity instance to which the field is being registered. */ export const registerFields = async ( + config: ApplicationPluginConfig, customField: RelationCustomEmbeddedFieldConfig, name: string, instance: any ): Promise => { if (customField.type === 'relation') { - if (customField.relationType === 'many-to-many') { - // Register Many-to-Many relation with additional options - const relationOptions = { - ...(customField.pivotTable && { pivotTable: customField.pivotTable }), - ...(customField.joinColumn && { joinColumn: customField.joinColumn }), - ...(customField.inverseJoinColumn && { inverseJoinColumn: customField.inverseJoinColumn }) - }; - // Register a Many-to-Many relation - MultiORMManyToMany(() => customField.entity, customField.inverseSide, relationOptions)(instance, name); - JoinTable({ name: customField.pivotTable })(instance, name); - } else if (customField.relationType === 'many-to-one') { - // Register a Many-to-One relation - MultiORMManyToOne(() => customField.entity, customField.inverseSide)(instance, name); - JoinColumn()(instance, name); + switch (customField.relationType) { + case 'many-to-many': { + const options = { + ...(customField.pivotTable && { pivotTable: customField.pivotTable }), + ...(customField.joinColumn && { joinColumn: customField.joinColumn }), + ...(customField.inverseJoinColumn && { inverseJoinColumn: customField.inverseJoinColumn }) + }; + MultiORMManyToMany(() => customField.entity, customField.inverseSide, options)(instance, name); + JoinTable({ name: customField.pivotTable })(instance, name); + break; + } + case 'many-to-one': { + const options = { + nullable: customField.nullable === false ? false : true, + unique: customField.unique ?? false, + ...(customField.onDelete && { onDelete: customField.onDelete }) + }; + MultiORMManyToOne(() => customField.entity, customField.inverseSide, options)(instance, name); + JoinColumn()(instance, name); + break; + } + default: + throw new Error(`Unsupported relation type: ${customField.relationType}`); } } else { - // Register a custom column - const { nullable, unique } = customField.options; - const options: ColumnDataType | ColumnOptions = { - name, - nullable: nullable === false ? false : true, - unique: unique ?? false - }; - MultiORMColumn(options)(instance, name); + defineColumn(config, customField, name, instance); } }; @@ -66,13 +107,13 @@ async function registerCustomFieldsForEntity( // Register each custom field await Promise.all( customFields.map(async (customField) => { - const { propertyPath } = customField; // Destructure to get property path - await registerFields(customField, propertyPath, instance); // Register the custom column + const { name } = customField; // Destructure to get property path + await registerFields(config, customField, name, instance); // Register the custom column }) ); /** - * If there are only relations are defined for an Entity for customFields, then TypeORM not saving realtions for entity ("Cannot set properties of undefined ()"). + * If there are only relations are defined for an Entity for customFields, then TypeORM not saving relations for entity ("Cannot set properties of undefined ()"). * So we have to add a "fake" column to the customFields embedded type to prevent this error from occurring. */ if (customFields.length > 0) { diff --git a/packages/core/src/core/entities/index.ts b/packages/core/src/core/entities/index.ts index 5624e3b2c78..dfa37158aaa 100644 --- a/packages/core/src/core/entities/index.ts +++ b/packages/core/src/core/entities/index.ts @@ -74,8 +74,6 @@ import { OrganizationDepartment, OrganizationDocument, OrganizationEmploymentType, - OrganizationGithubRepository, - OrganizationGithubRepositoryIssue, OrganizationLanguage, OrganizationPosition, OrganizationProject, @@ -214,8 +212,6 @@ export const coreEntities = [ OrganizationDepartment, OrganizationDocument, OrganizationEmploymentType, - OrganizationGithubRepository, - OrganizationGithubRepositoryIssue, OrganizationLanguage, OrganizationPosition, OrganizationProject, diff --git a/packages/core/src/core/entities/internal.ts b/packages/core/src/core/entities/internal.ts index 00f9634c567..17ac73e1fe4 100644 --- a/packages/core/src/core/entities/internal.ts +++ b/packages/core/src/core/entities/internal.ts @@ -61,8 +61,6 @@ export * from '../../integration-entity-setting/integration-entity-setting.entit export * from '../../integration-map/integration-map.entity'; export * from '../../integration-setting/integration-setting.entity'; export * from '../../integration-tenant/integration-tenant.entity'; -export * from '../../integration/github/repository/github-repository.entity'; -export * from '../../integration/github/repository/issue/github-repository-issue.entity'; export * from '../../integration/integration-type.entity'; export * from '../../integration/integration.entity'; export * from '../../invite/invite.entity'; diff --git a/packages/core/src/core/entities/subscribers/entity-event-subscriber.types.ts b/packages/core/src/core/entities/subscribers/entity-event-subscriber.types.ts index 26d7b46d3cc..00ed930e176 100644 --- a/packages/core/src/core/entities/subscribers/entity-event-subscriber.types.ts +++ b/packages/core/src/core/entities/subscribers/entity-event-subscriber.types.ts @@ -1,94 +1,76 @@ import { EntityManager as MikroOrmEntityManager } from '@mikro-orm/core'; import { EntityManager as TypeOrmEntityManager } from 'typeorm'; -export { MikroOrmEntityManager, TypeOrmEntityManager } +export { MikroOrmEntityManager, TypeOrmEntityManager }; export type MultiOrmEntityManager = MikroOrmEntityManager | TypeOrmEntityManager; export interface IEntityEventSubscriber { - /** - * Optional method to specify the entity class that this subscriber listens to. - * It should return either a constructor function (a class) or a string representing the name of the entity. - * - * @returns {Function | string} The entity class or its name. - */ - listenTo?(): Function | string; + /** + * Optional method to specify the entity class that this subscriber listens to. + * It should return either a constructor function (a class) or a string representing the name of the entity. + * + * @returns {Function | string} The entity class or its name. + */ + listenTo?(): Function | string; - /** - * Optional method that is called before an entity is created. - * Implement this method to define specific logic to be executed just before the creation of an entity - * - * @param entity The entity that is about to be created. - * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. - * @returns {Promise} - */ - beforeEntityCreate( - entity: Entity, - em?: MultiOrmEntityManager - ): Promise; + /** + * Optional method that is called before an entity is created. + * Implement this method to define specific logic to be executed just before the creation of an entity + * + * @param entity The entity that is about to be created. + * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. + * @returns {Promise} + */ + beforeEntityCreate(entity: Entity, em?: MultiOrmEntityManager): Promise; - /** - * Optional method that is called before an entity is updated. - * Implement this method to define specific logic to be executed just before the updation of an entity - * - * @param entity The entity that is about to be updated. - * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. - * @returns {Promise} - */ - beforeEntityUpdate( - entity: Entity, - em?: MultiOrmEntityManager - ): Promise; + /** + * Optional method that is called before an entity is updated. + * Implement this method to define specific logic to be executed just before the update of an entity + * + * @param entity The entity that is about to be updated. + * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. + * @returns {Promise} + */ + beforeEntityUpdate(entity: Entity, em?: MultiOrmEntityManager): Promise; - /** - * Optional method that is called after an entity is updated. - * Implement this method to define specific logic to be executed just before the updation of an entity - * - * @param entity The entity that is about to be updated. - * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. - * @returns {Promise} - */ - afterEntityUpdate( - entity: Entity, - em?: MultiOrmEntityManager - ): Promise; + /** + * Optional method that is called after an entity is updated. + * Implement this method to define specific logic to be executed just before the update of an entity + * + * @param entity The entity that is about to be updated. + * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. + * @returns {Promise} + */ + afterEntityUpdate(entity: Entity, em?: MultiOrmEntityManager): Promise; - /** - * Optional method that is called after an entity is created. - * Implement this method to define specific logic to be executed after an entity creation event. - * - * @param entity The entity that was created. - * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. - * @returns {Promise} - */ - afterEntityCreate( - entity: Entity, - em?: MultiOrmEntityManager - ): Promise; + /** + * Optional method that is called after an entity is created. + * Implement this method to define specific logic to be executed after an entity creation event. + * + * @param entity The entity that was created. + * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. + * @returns {Promise} + */ + afterEntityCreate(entity: Entity, em?: MultiOrmEntityManager): Promise; - /** - * Optional method that is called after an entity is loaded. - * Implement this method to define specific logic to be executed after an entity loading event. - * - * @param entity The entity that was loaded. - * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. - * @returns {Promise} - */ - afterEntityLoad( - entity: Entity, - em?: MultiOrmEntityManager - ): Promise; + /** + * Optional method that is called after an entity is loaded. + * Implement this method to define specific logic to be executed after an entity loading event. + * + * @param entity The entity that was loaded. + * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. + * @returns {Promise} + */ + afterEntityLoad(entity: Entity, em?: MultiOrmEntityManager): Promise; - /** - * Optional method that is called after an entity is deleted. - * Implement this method to define specific logic to be executed after an entity deletion event. - * - * @param entity The entity that has been deleted. - * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. - * @returns {Promise} - */ - afterEntityDelete( - entity: Entity, - em?: MultiOrmEntityManager - ): Promise; + /** + * Optional method that is called after an entity is deleted. + * Implement this method to define specific logic to be executed after an entity deletion event. + * + * @param entity The entity that has been deleted. + * @param em An optional entity manager which can be either from TypeORM or MikroORM, used for further database operations if needed. + * @returns {Promise} + */ + afterEntityDelete(entity: Entity, em?: MultiOrmEntityManager): Promise; } diff --git a/packages/core/src/core/interceptors/lazy-file-interceptor.ts b/packages/core/src/core/interceptors/lazy-file-interceptor.ts index d1db5af6b35..c06ad1ebc5d 100644 --- a/packages/core/src/core/interceptors/lazy-file-interceptor.ts +++ b/packages/core/src/core/interceptors/lazy-file-interceptor.ts @@ -1,12 +1,4 @@ -import { - CallHandler, - ExecutionContext, - Inject, - mixin, - NestInterceptor, - Optional, - Type, -} from '@nestjs/common'; +import { CallHandler, ExecutionContext, Inject, mixin, NestInterceptor, Optional, Type } from '@nestjs/common'; import { MulterModuleOptions } from '@nestjs/platform-express'; import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; import { MULTER_MODULE_OPTIONS } from '@nestjs/platform-express/multer/files.constants'; @@ -16,50 +8,39 @@ import { Observable } from 'rxjs'; type MulterInstance = any; -export function LazyFileInterceptor( - fieldName: string, - localOptions?: MulterOptions, -): Type { +export function LazyFileInterceptor(fieldName: string, localOptions?: MulterOptions): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; - class MixinInterceptor implements NestInterceptor { - protected multer: MulterInstance; + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + private options: MulterModuleOptions = {} + ) {} - constructor( - @Optional() - @Inject(MULTER_MODULE_OPTIONS) - private options: MulterModuleOptions = {}, - ) { } + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const ctx = context.switchToHttp(); + const storage = localOptions.storage(context); - async intercept( - context: ExecutionContext, - next: CallHandler, - ): Promise> { - const ctx = context.switchToHttp(); - const storage = localOptions.storage(context); - - this.multer = (multer as any)({ - ...this.options, - ...{ - storage - } - }); - await new Promise((resolve, reject) => - this.multer.single(fieldName)( - ctx.getRequest(), - ctx.getResponse(), - (err: any) => { - if (err) { - const error = transformException(err); - console.log('Error while uploading file using multer', err); - return reject(error); - } - resolve(); - }, - ), - ); - return next.handle(); - } - } - const Interceptor = mixin(MixinInterceptor); - return Interceptor; + this.multer = (multer as any)({ + ...this.options, + ...{ + storage + } + }); + await new Promise((resolve, reject) => + this.multer.single(fieldName)(ctx.getRequest(), ctx.getResponse(), (err: any) => { + if (err) { + const error = transformException(err); + console.log('Error while uploading file using multer', err); + return reject(error); + } + resolve(); + }) + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor; } diff --git a/packages/core/src/core/interceptors/serializer.interceptor.ts b/packages/core/src/core/interceptors/serializer.interceptor.ts index 209b4c1e46e..231c25f0df3 100644 --- a/packages/core/src/core/interceptors/serializer.interceptor.ts +++ b/packages/core/src/core/interceptors/serializer.interceptor.ts @@ -1,10 +1,4 @@ -import { - Injectable, - ExecutionContext, - CallHandler, - ClassSerializerInterceptor, - NestInterceptor -} from '@nestjs/common'; +import { Injectable, ExecutionContext, CallHandler, ClassSerializerInterceptor, NestInterceptor } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { instanceToPlain } from 'class-transformer'; @@ -15,20 +9,24 @@ import { RequestContext } from './../../core/context'; @Injectable() export class SerializerInterceptor extends ClassSerializerInterceptor implements NestInterceptor { - - intercept( - ctx: ExecutionContext, - next: CallHandler - ): Observable { + /** + * Intercepts the response and transforms the data based on the user's role. + * + * @param ctx - The execution context. + * @param next - The call handler. + * @returns An observable that represents the intercepted response. + */ + intercept(ctx: ExecutionContext, next: CallHandler): Observable { + // Extract the current token from the request context const token = RequestContext.currentToken(); + + // Verify the token and extract the user's role const { role } = verify(token, environment.JWT_SECRET) as { id: string; role: RolesEnum; }; - return next - .handle() - .pipe( - map((data) => instanceToPlain(data, { groups: [role] })) - ); + + // Handle the response and transform the data based on the role + return next.handle().pipe(map((data) => instanceToPlain(data, { groups: [role] }))); } -} \ No newline at end of file +} diff --git a/packages/core/src/core/interceptors/transform.interceptor.ts b/packages/core/src/core/interceptors/transform.interceptor.ts index 735857d711e..cbc274c4159 100644 --- a/packages/core/src/core/interceptors/transform.interceptor.ts +++ b/packages/core/src/core/interceptors/transform.interceptor.ts @@ -6,13 +6,12 @@ import { HttpException, BadRequestException } from '@nestjs/common'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { instanceToPlain } from 'class-transformer'; @Injectable() export class TransformInterceptor implements NestInterceptor { - /** * Intercepts the execution context and the call handler. * Transforms the data using class-transformer's instanceToPlain. @@ -29,14 +28,10 @@ export class TransformInterceptor implements NestInterceptor { catchError((error: any) => { // If it's a BadRequestException, return a new instance of BadRequestException if (error instanceof BadRequestException) { - return of( - new BadRequestException(error.getResponse()) - ); + throw new BadRequestException(error.getResponse()); } // For other errors, return a new instance of HttpException - return of( - new HttpException(error.message, error.status) - ); + throw new HttpException(error.message, error.status); }) ); } diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index b6a394a1ab6..9489b656984 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -1,3 +1,3 @@ -export { createMigration, generateMigration, revertLastDatabaseMigration, runDatabaseMigrations } from './migration-executor'; -export { ConnectionEntityManager } from './connection-entity-manager'; -export { prepareSQLQuery } from './database.helper'; +export * from './migration-executor'; +export * from './connection-entity-manager'; +export * from './database.helper'; diff --git a/packages/core/src/database/migrations/1719937371312-AlterOrganizationGithubRepositoryEntityTable.ts b/packages/core/src/database/migrations/1719937371312-AlterOrganizationGithubRepositoryEntityTable.ts new file mode 100644 index 00000000000..d3d600e5b17 --- /dev/null +++ b/packages/core/src/database/migrations/1719937371312-AlterOrganizationGithubRepositoryEntityTable.ts @@ -0,0 +1,385 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AlterOrganizationGithubRepositoryEntityTable1719937371312 implements MigrationInterface { + name = 'AlterOrganizationGithubRepositoryEntityTable1719937371312'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "public"."IDX_ca0fa80f50baed7287a499dc2c"`); + await queryRunner.query(`ALTER TABLE "organization_github_repository" ALTER COLUMN "repositoryId" TYPE bigint`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository" ALTER COLUMN "repositoryId" SET NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ca0fa80f50baed7287a499dc2c" ON "organization_github_repository" ("repositoryId")` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_ca0fa80f50baed7287a499dc2c"`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository" ALTER COLUMN "repositoryId" TYPE integer` + ); + await queryRunner.query( + `ALTER TABLE "organization_github_repository" ALTER COLUMN "repositoryId" SET NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ca0fa80f50baed7287a499dc2c" ON "organization_github_repository" ("repositoryId") ` + ); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_59407d03d189560ac1a0a4b0eb"`); + await queryRunner.query(`DROP INDEX "IDX_2eec784cadcb7847b64937fb58"`); + await queryRunner.query(`DROP INDEX "IDX_34c48d11eb82ef42e89370bdc7"`); + await queryRunner.query(`DROP INDEX "IDX_04717f25bea7d9cef0d51cac50"`); + await queryRunner.query(`DROP INDEX "IDX_5e97728cfda96f49cc7f95bbaf"`); + await queryRunner.query(`DROP INDEX "IDX_ef65338e8597b9f56fd0fe3c94"`); + await queryRunner.query(`DROP INDEX "IDX_480158f21938444e4f62fb3185"`); + await queryRunner.query(`DROP INDEX "IDX_69d75a47af6bfcda545a865691"`); + await queryRunner.query(`DROP INDEX "IDX_ca0fa80f50baed7287a499dc2c"`); + await queryRunner.query(`DROP INDEX "IDX_6eea42a69e130bbd14b7ea3659"`); + await queryRunner.query(`DROP INDEX "IDX_a146e202c19f521bf5ec69bb26"`); + await queryRunner.query(`DROP INDEX "IDX_9e8a77c1d330554fab9230100a"`); + await queryRunner.query(`DROP INDEX "IDX_add7dbec156589dd0b27e2e0c4"`); + await queryRunner.query( + `CREATE TABLE "temporary_organization_github_repository" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "repositoryId" integer NOT NULL, "name" varchar NOT NULL, "fullName" varchar NOT NULL, "owner" varchar NOT NULL, "integrationId" varchar, "issuesCount" integer, "hasSyncEnabled" boolean DEFAULT (1), "private" boolean DEFAULT (0), "status" varchar, "deletedAt" datetime, CONSTRAINT "FK_480158f21938444e4f62fb31857" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_69d75a47af6bfcda545a865691b" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_add7dbec156589dd0b27e2e0c49" FOREIGN KEY ("integrationId") REFERENCES "integration_tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_organization_github_repository"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt" FROM "organization_github_repository"` + ); + await queryRunner.query(`DROP TABLE "organization_github_repository"`); + await queryRunner.query( + `ALTER TABLE "temporary_organization_github_repository" RENAME TO "organization_github_repository"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_59407d03d189560ac1a0a4b0eb" ON "organization_github_repository" ("status") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2eec784cadcb7847b64937fb58" ON "organization_github_repository" ("private") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_34c48d11eb82ef42e89370bdc7" ON "organization_github_repository" ("hasSyncEnabled") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_04717f25bea7d9cef0d51cac50" ON "organization_github_repository" ("issuesCount") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5e97728cfda96f49cc7f95bbaf" ON "organization_github_repository" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ef65338e8597b9f56fd0fe3c94" ON "organization_github_repository" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_480158f21938444e4f62fb3185" ON "organization_github_repository" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_69d75a47af6bfcda545a865691" ON "organization_github_repository" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ca0fa80f50baed7287a499dc2c" ON "organization_github_repository" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6eea42a69e130bbd14b7ea3659" ON "organization_github_repository" ("name") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a146e202c19f521bf5ec69bb26" ON "organization_github_repository" ("fullName") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9e8a77c1d330554fab9230100a" ON "organization_github_repository" ("owner") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_add7dbec156589dd0b27e2e0c4" ON "organization_github_repository" ("integrationId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_59407d03d189560ac1a0a4b0eb"`); + await queryRunner.query(`DROP INDEX "IDX_2eec784cadcb7847b64937fb58"`); + await queryRunner.query(`DROP INDEX "IDX_34c48d11eb82ef42e89370bdc7"`); + await queryRunner.query(`DROP INDEX "IDX_04717f25bea7d9cef0d51cac50"`); + await queryRunner.query(`DROP INDEX "IDX_5e97728cfda96f49cc7f95bbaf"`); + await queryRunner.query(`DROP INDEX "IDX_ef65338e8597b9f56fd0fe3c94"`); + await queryRunner.query(`DROP INDEX "IDX_480158f21938444e4f62fb3185"`); + await queryRunner.query(`DROP INDEX "IDX_69d75a47af6bfcda545a865691"`); + await queryRunner.query(`DROP INDEX "IDX_ca0fa80f50baed7287a499dc2c"`); + await queryRunner.query(`DROP INDEX "IDX_6eea42a69e130bbd14b7ea3659"`); + await queryRunner.query(`DROP INDEX "IDX_a146e202c19f521bf5ec69bb26"`); + await queryRunner.query(`DROP INDEX "IDX_9e8a77c1d330554fab9230100a"`); + await queryRunner.query(`DROP INDEX "IDX_add7dbec156589dd0b27e2e0c4"`); + await queryRunner.query( + `CREATE TABLE "temporary_organization_github_repository" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "repositoryId" bigint NOT NULL, "name" varchar NOT NULL, "fullName" varchar NOT NULL, "owner" varchar NOT NULL, "integrationId" varchar, "issuesCount" integer, "hasSyncEnabled" boolean DEFAULT (1), "private" boolean DEFAULT (0), "status" varchar, "deletedAt" datetime, CONSTRAINT "FK_480158f21938444e4f62fb31857" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_69d75a47af6bfcda545a865691b" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_add7dbec156589dd0b27e2e0c49" FOREIGN KEY ("integrationId") REFERENCES "integration_tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_organization_github_repository"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt" FROM "organization_github_repository"` + ); + await queryRunner.query(`DROP TABLE "organization_github_repository"`); + await queryRunner.query( + `ALTER TABLE "temporary_organization_github_repository" RENAME TO "organization_github_repository"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_59407d03d189560ac1a0a4b0eb" ON "organization_github_repository" ("status") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2eec784cadcb7847b64937fb58" ON "organization_github_repository" ("private") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_34c48d11eb82ef42e89370bdc7" ON "organization_github_repository" ("hasSyncEnabled") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_04717f25bea7d9cef0d51cac50" ON "organization_github_repository" ("issuesCount") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5e97728cfda96f49cc7f95bbaf" ON "organization_github_repository" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ef65338e8597b9f56fd0fe3c94" ON "organization_github_repository" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_480158f21938444e4f62fb3185" ON "organization_github_repository" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_69d75a47af6bfcda545a865691" ON "organization_github_repository" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ca0fa80f50baed7287a499dc2c" ON "organization_github_repository" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6eea42a69e130bbd14b7ea3659" ON "organization_github_repository" ("name") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a146e202c19f521bf5ec69bb26" ON "organization_github_repository" ("fullName") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9e8a77c1d330554fab9230100a" ON "organization_github_repository" ("owner") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_add7dbec156589dd0b27e2e0c4" ON "organization_github_repository" ("integrationId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_7ae5b4d4bdec77971dab319f2e"`); + await queryRunner.query(`DROP INDEX "IDX_68e75e49f06409fd385b4f8774"`); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_add7dbec156589dd0b27e2e0c4"`); + await queryRunner.query(`DROP INDEX "IDX_9e8a77c1d330554fab9230100a"`); + await queryRunner.query(`DROP INDEX "IDX_a146e202c19f521bf5ec69bb26"`); + await queryRunner.query(`DROP INDEX "IDX_6eea42a69e130bbd14b7ea3659"`); + await queryRunner.query(`DROP INDEX "IDX_ca0fa80f50baed7287a499dc2c"`); + await queryRunner.query(`DROP INDEX "IDX_69d75a47af6bfcda545a865691"`); + await queryRunner.query(`DROP INDEX "IDX_480158f21938444e4f62fb3185"`); + await queryRunner.query(`DROP INDEX "IDX_ef65338e8597b9f56fd0fe3c94"`); + await queryRunner.query(`DROP INDEX "IDX_5e97728cfda96f49cc7f95bbaf"`); + await queryRunner.query(`DROP INDEX "IDX_04717f25bea7d9cef0d51cac50"`); + await queryRunner.query(`DROP INDEX "IDX_34c48d11eb82ef42e89370bdc7"`); + await queryRunner.query(`DROP INDEX "IDX_2eec784cadcb7847b64937fb58"`); + await queryRunner.query(`DROP INDEX "IDX_59407d03d189560ac1a0a4b0eb"`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository" RENAME TO "temporary_organization_github_repository"` + ); + await queryRunner.query( + `CREATE TABLE "organization_github_repository" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "repositoryId" integer NOT NULL, "name" varchar NOT NULL, "fullName" varchar NOT NULL, "owner" varchar NOT NULL, "integrationId" varchar, "issuesCount" integer, "hasSyncEnabled" boolean DEFAULT (1), "private" boolean DEFAULT (0), "status" varchar, "deletedAt" datetime, CONSTRAINT "FK_480158f21938444e4f62fb31857" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_69d75a47af6bfcda545a865691b" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_add7dbec156589dd0b27e2e0c49" FOREIGN KEY ("integrationId") REFERENCES "integration_tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "organization_github_repository"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt" FROM "temporary_organization_github_repository"` + ); + await queryRunner.query(`DROP TABLE "temporary_organization_github_repository"`); + await queryRunner.query( + `CREATE INDEX "IDX_add7dbec156589dd0b27e2e0c4" ON "organization_github_repository" ("integrationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9e8a77c1d330554fab9230100a" ON "organization_github_repository" ("owner") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a146e202c19f521bf5ec69bb26" ON "organization_github_repository" ("fullName") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6eea42a69e130bbd14b7ea3659" ON "organization_github_repository" ("name") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ca0fa80f50baed7287a499dc2c" ON "organization_github_repository" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_69d75a47af6bfcda545a865691" ON "organization_github_repository" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_480158f21938444e4f62fb3185" ON "organization_github_repository" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ef65338e8597b9f56fd0fe3c94" ON "organization_github_repository" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5e97728cfda96f49cc7f95bbaf" ON "organization_github_repository" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_04717f25bea7d9cef0d51cac50" ON "organization_github_repository" ("issuesCount") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_34c48d11eb82ef42e89370bdc7" ON "organization_github_repository" ("hasSyncEnabled") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2eec784cadcb7847b64937fb58" ON "organization_github_repository" ("private") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_59407d03d189560ac1a0a4b0eb" ON "organization_github_repository" ("status") ` + ); + await queryRunner.query(`DROP INDEX "IDX_add7dbec156589dd0b27e2e0c4"`); + await queryRunner.query(`DROP INDEX "IDX_9e8a77c1d330554fab9230100a"`); + await queryRunner.query(`DROP INDEX "IDX_a146e202c19f521bf5ec69bb26"`); + await queryRunner.query(`DROP INDEX "IDX_6eea42a69e130bbd14b7ea3659"`); + await queryRunner.query(`DROP INDEX "IDX_ca0fa80f50baed7287a499dc2c"`); + await queryRunner.query(`DROP INDEX "IDX_69d75a47af6bfcda545a865691"`); + await queryRunner.query(`DROP INDEX "IDX_480158f21938444e4f62fb3185"`); + await queryRunner.query(`DROP INDEX "IDX_ef65338e8597b9f56fd0fe3c94"`); + await queryRunner.query(`DROP INDEX "IDX_5e97728cfda96f49cc7f95bbaf"`); + await queryRunner.query(`DROP INDEX "IDX_04717f25bea7d9cef0d51cac50"`); + await queryRunner.query(`DROP INDEX "IDX_34c48d11eb82ef42e89370bdc7"`); + await queryRunner.query(`DROP INDEX "IDX_2eec784cadcb7847b64937fb58"`); + await queryRunner.query(`DROP INDEX "IDX_59407d03d189560ac1a0a4b0eb"`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository" RENAME TO "temporary_organization_github_repository"` + ); + await queryRunner.query( + `CREATE TABLE "organization_github_repository" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "repositoryId" integer NOT NULL, "name" varchar NOT NULL, "fullName" varchar NOT NULL, "owner" varchar NOT NULL, "integrationId" varchar, "issuesCount" integer, "hasSyncEnabled" boolean DEFAULT (1), "private" boolean DEFAULT (0), "status" varchar, "deletedAt" datetime, CONSTRAINT "FK_480158f21938444e4f62fb31857" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_69d75a47af6bfcda545a865691b" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_add7dbec156589dd0b27e2e0c49" FOREIGN KEY ("integrationId") REFERENCES "integration_tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "organization_github_repository"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "repositoryId", "name", "fullName", "owner", "integrationId", "issuesCount", "hasSyncEnabled", "private", "status", "deletedAt" FROM "temporary_organization_github_repository"` + ); + await queryRunner.query(`DROP TABLE "temporary_organization_github_repository"`); + await queryRunner.query( + `CREATE INDEX "IDX_add7dbec156589dd0b27e2e0c4" ON "organization_github_repository" ("integrationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9e8a77c1d330554fab9230100a" ON "organization_github_repository" ("owner") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a146e202c19f521bf5ec69bb26" ON "organization_github_repository" ("fullName") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6eea42a69e130bbd14b7ea3659" ON "organization_github_repository" ("name") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ca0fa80f50baed7287a499dc2c" ON "organization_github_repository" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_69d75a47af6bfcda545a865691" ON "organization_github_repository" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_480158f21938444e4f62fb3185" ON "organization_github_repository" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_ef65338e8597b9f56fd0fe3c94" ON "organization_github_repository" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5e97728cfda96f49cc7f95bbaf" ON "organization_github_repository" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_04717f25bea7d9cef0d51cac50" ON "organization_github_repository" ("issuesCount") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_34c48d11eb82ef42e89370bdc7" ON "organization_github_repository" ("hasSyncEnabled") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_2eec784cadcb7847b64937fb58" ON "organization_github_repository" ("private") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_59407d03d189560ac1a0a4b0eb" ON "organization_github_repository" ("status") ` + ); + await queryRunner.query(`DROP INDEX "IDX_68e75e49f06409fd385b4f8774"`); + await queryRunner.query(`DROP INDEX "IDX_7ae5b4d4bdec77971dab319f2e"`); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX \`IDX_ca0fa80f50baed7287a499dc2c\` ON \`organization_github_repository\``); + await queryRunner.query( + `ALTER TABLE \`organization_github_repository\` MODIFY COLUMN \`repositoryId\` bigint NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX \`IDX_ca0fa80f50baed7287a499dc2c\` ON \`organization_github_repository\` (\`repositoryId\`)` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX \`IDX_ca0fa80f50baed7287a499dc2c\` ON \`organization_github_repository\``); + await queryRunner.query( + `ALTER TABLE \`organization_github_repository\` MODIFY COLUMN \`repositoryId\` int NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX \`IDX_ca0fa80f50baed7287a499dc2c\` ON \`organization_github_repository\` (\`repositoryId\`)` + ); + } +} diff --git a/packages/core/src/database/migrations/1719994643595-AlterOrganizationGithubRepositoryIssueEntityTable.ts b/packages/core/src/database/migrations/1719994643595-AlterOrganizationGithubRepositoryIssueEntityTable.ts new file mode 100644 index 00000000000..78ab79cde06 --- /dev/null +++ b/packages/core/src/database/migrations/1719994643595-AlterOrganizationGithubRepositoryIssueEntityTable.ts @@ -0,0 +1,291 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AlterOrganizationGithubRepositoryIssueEntityTable1719994643595 implements MigrationInterface { + name = 'AlterOrganizationGithubRepositoryIssueEntityTable1719994643595'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_055f310a04a928343494a5255a"`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository_issue" ALTER COLUMN "issueId" TYPE bigint` + ); + await queryRunner.query( + `ALTER TABLE "organization_github_repository_issue" ALTER COLUMN "issueId" SET NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX "IDX_055f310a04a928343494a5255a" ON "organization_github_repository_issue" ("issueId") ` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_055f310a04a928343494a5255a"`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository_issue" ALTER COLUMN "issueId" TYPE integer` + ); + await queryRunner.query( + `ALTER TABLE "organization_github_repository_issue" ALTER COLUMN "issueId" SET NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX "IDX_055f310a04a928343494a5255a" ON "organization_github_repository_issue" ("issueId") ` + ); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_d706210d377ece2a1bc3386388"`); + await queryRunner.query(`DROP INDEX "IDX_c774c276d6b7ea05a7e12d3c81"`); + await queryRunner.query(`DROP INDEX "IDX_b3234be5b70c2362cdf67bb188"`); + await queryRunner.query(`DROP INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b"`); + await queryRunner.query(`DROP INDEX "IDX_055f310a04a928343494a5255a"`); + await queryRunner.query(`DROP INDEX "IDX_a8709a9c5cc142c6fbe92df274"`); + await queryRunner.query(`DROP INDEX "IDX_5065401113abb6e9608225e567"`); + await queryRunner.query( + `CREATE TABLE "temporary_organization_github_repository_issue" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "issueId" integer NOT NULL, "issueNumber" integer NOT NULL, "repositoryId" varchar, "deletedAt" datetime, CONSTRAINT "FK_b3234be5b70c2362cdf67bb1889" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6c8e119fc6a2a7d3413aa76d3bd" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_5065401113abb6e9608225e5678" FOREIGN KEY ("repositoryId") REFERENCES "organization_github_repository" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_organization_github_repository_issue"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt" FROM "organization_github_repository_issue"` + ); + await queryRunner.query(`DROP TABLE "organization_github_repository_issue"`); + await queryRunner.query( + `ALTER TABLE "temporary_organization_github_repository_issue" RENAME TO "organization_github_repository_issue"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_d706210d377ece2a1bc3386388" ON "organization_github_repository_issue" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_c774c276d6b7ea05a7e12d3c81" ON "organization_github_repository_issue" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b3234be5b70c2362cdf67bb188" ON "organization_github_repository_issue" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b" ON "organization_github_repository_issue" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_055f310a04a928343494a5255a" ON "organization_github_repository_issue" ("issueId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a8709a9c5cc142c6fbe92df274" ON "organization_github_repository_issue" ("issueNumber") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5065401113abb6e9608225e567" ON "organization_github_repository_issue" ("repositoryId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_d706210d377ece2a1bc3386388"`); + await queryRunner.query(`DROP INDEX "IDX_c774c276d6b7ea05a7e12d3c81"`); + await queryRunner.query(`DROP INDEX "IDX_b3234be5b70c2362cdf67bb188"`); + await queryRunner.query(`DROP INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b"`); + await queryRunner.query(`DROP INDEX "IDX_055f310a04a928343494a5255a"`); + await queryRunner.query(`DROP INDEX "IDX_a8709a9c5cc142c6fbe92df274"`); + await queryRunner.query(`DROP INDEX "IDX_5065401113abb6e9608225e567"`); + await queryRunner.query( + `CREATE TABLE "temporary_organization_github_repository_issue" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "issueId" bigint NOT NULL, "issueNumber" integer NOT NULL, "repositoryId" varchar, "deletedAt" datetime, CONSTRAINT "FK_b3234be5b70c2362cdf67bb1889" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6c8e119fc6a2a7d3413aa76d3bd" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_5065401113abb6e9608225e5678" FOREIGN KEY ("repositoryId") REFERENCES "organization_github_repository" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_organization_github_repository_issue"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt" FROM "organization_github_repository_issue"` + ); + await queryRunner.query(`DROP TABLE "organization_github_repository_issue"`); + await queryRunner.query( + `ALTER TABLE "temporary_organization_github_repository_issue" RENAME TO "organization_github_repository_issue"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_d706210d377ece2a1bc3386388" ON "organization_github_repository_issue" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_c774c276d6b7ea05a7e12d3c81" ON "organization_github_repository_issue" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b3234be5b70c2362cdf67bb188" ON "organization_github_repository_issue" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b" ON "organization_github_repository_issue" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_055f310a04a928343494a5255a" ON "organization_github_repository_issue" ("issueId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a8709a9c5cc142c6fbe92df274" ON "organization_github_repository_issue" ("issueNumber") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_5065401113abb6e9608225e567" ON "organization_github_repository_issue" ("repositoryId") ` + ); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5065401113abb6e9608225e567"`); + await queryRunner.query(`DROP INDEX "IDX_a8709a9c5cc142c6fbe92df274"`); + await queryRunner.query(`DROP INDEX "IDX_055f310a04a928343494a5255a"`); + await queryRunner.query(`DROP INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b"`); + await queryRunner.query(`DROP INDEX "IDX_b3234be5b70c2362cdf67bb188"`); + await queryRunner.query(`DROP INDEX "IDX_c774c276d6b7ea05a7e12d3c81"`); + await queryRunner.query(`DROP INDEX "IDX_d706210d377ece2a1bc3386388"`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository_issue" RENAME TO "temporary_organization_github_repository_issue"` + ); + await queryRunner.query( + `CREATE TABLE "organization_github_repository_issue" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "issueId" integer NOT NULL, "issueNumber" integer NOT NULL, "repositoryId" varchar, "deletedAt" datetime, CONSTRAINT "FK_b3234be5b70c2362cdf67bb1889" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6c8e119fc6a2a7d3413aa76d3bd" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_5065401113abb6e9608225e5678" FOREIGN KEY ("repositoryId") REFERENCES "organization_github_repository" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "organization_github_repository_issue"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt" FROM "temporary_organization_github_repository_issue"` + ); + await queryRunner.query(`DROP TABLE "temporary_organization_github_repository_issue"`); + await queryRunner.query( + `CREATE INDEX "IDX_5065401113abb6e9608225e567" ON "organization_github_repository_issue" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a8709a9c5cc142c6fbe92df274" ON "organization_github_repository_issue" ("issueNumber") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_055f310a04a928343494a5255a" ON "organization_github_repository_issue" ("issueId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b" ON "organization_github_repository_issue" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b3234be5b70c2362cdf67bb188" ON "organization_github_repository_issue" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_c774c276d6b7ea05a7e12d3c81" ON "organization_github_repository_issue" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_d706210d377ece2a1bc3386388" ON "organization_github_repository_issue" ("isActive") ` + ); + await queryRunner.query(`DROP INDEX "IDX_5065401113abb6e9608225e567"`); + await queryRunner.query(`DROP INDEX "IDX_a8709a9c5cc142c6fbe92df274"`); + await queryRunner.query(`DROP INDEX "IDX_055f310a04a928343494a5255a"`); + await queryRunner.query(`DROP INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b"`); + await queryRunner.query(`DROP INDEX "IDX_b3234be5b70c2362cdf67bb188"`); + await queryRunner.query(`DROP INDEX "IDX_c774c276d6b7ea05a7e12d3c81"`); + await queryRunner.query(`DROP INDEX "IDX_d706210d377ece2a1bc3386388"`); + await queryRunner.query( + `ALTER TABLE "organization_github_repository_issue" RENAME TO "temporary_organization_github_repository_issue"` + ); + await queryRunner.query( + `CREATE TABLE "organization_github_repository_issue" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "tenantId" varchar, "organizationId" varchar, "issueId" integer NOT NULL, "issueNumber" integer NOT NULL, "repositoryId" varchar, "deletedAt" datetime, CONSTRAINT "FK_b3234be5b70c2362cdf67bb1889" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6c8e119fc6a2a7d3413aa76d3bd" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_5065401113abb6e9608225e5678" FOREIGN KEY ("repositoryId") REFERENCES "organization_github_repository" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "organization_github_repository_issue"("id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt") SELECT "id", "createdAt", "updatedAt", "isActive", "isArchived", "tenantId", "organizationId", "issueId", "issueNumber", "repositoryId", "deletedAt" FROM "temporary_organization_github_repository_issue"` + ); + await queryRunner.query(`DROP TABLE "temporary_organization_github_repository_issue"`); + await queryRunner.query( + `CREATE INDEX "IDX_5065401113abb6e9608225e567" ON "organization_github_repository_issue" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_a8709a9c5cc142c6fbe92df274" ON "organization_github_repository_issue" ("issueNumber") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_055f310a04a928343494a5255a" ON "organization_github_repository_issue" ("issueId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6c8e119fc6a2a7d3413aa76d3b" ON "organization_github_repository_issue" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_b3234be5b70c2362cdf67bb188" ON "organization_github_repository_issue" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_c774c276d6b7ea05a7e12d3c81" ON "organization_github_repository_issue" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_d706210d377ece2a1bc3386388" ON "organization_github_repository_issue" ("isActive") ` + ); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX \`IDX_055f310a04a928343494a5255a\` ON \`organization_github_repository_issue\`` + ); + await queryRunner.query( + `ALTER TABLE \`organization_github_repository_issue\` MODIFY COLUMN \`issueId\` bigint NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX \`IDX_055f310a04a928343494a5255a\` ON \`organization_github_repository_issue\` (\`issueId\`)` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX \`IDX_055f310a04a928343494a5255a\` ON \`organization_github_repository_issue\`` + ); + await queryRunner.query( + `ALTER TABLE \`organization_github_repository_issue\` MODIFY COLUMN \`issueId\` int NOT NULL` + ); + await queryRunner.query( + `CREATE INDEX \`IDX_055f310a04a928343494a5255a\` ON \`organization_github_repository_issue\` (\`issueId\`)` + ); + } +} diff --git a/packages/core/src/database/migrations/1720177290238-AlterOrganizationProjectCustomEntityFields.ts b/packages/core/src/database/migrations/1720177290238-AlterOrganizationProjectCustomEntityFields.ts new file mode 100644 index 00000000000..cc5b72fcf61 --- /dev/null +++ b/packages/core/src/database/migrations/1720177290238-AlterOrganizationProjectCustomEntityFields.ts @@ -0,0 +1,263 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AlterOrganizationProjectCustomEntityFields1720177290238 implements MigrationInterface { + name = 'AlterOrganizationProjectCustomEntityFields1720177290238'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(yellow(this.name + ' start running!')); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "organization_project" ADD "fix_relational_custom_fields" boolean`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "organization_project" DROP COLUMN "fix_relational_custom_fields"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_employee_job_preset" ("jobPresetId" varchar NOT NULL, "employeeId" varchar NOT NULL, CONSTRAINT "FK_68e75e49f06409fd385b4f87746" FOREIGN KEY ("employeeId") REFERENCES "employee" ("id") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY ("jobPresetId", "employeeId"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_employee_job_preset"("jobPresetId", "employeeId") SELECT "jobPresetId", "employeeId" FROM "employee_job_preset"` + ); + await queryRunner.query(`DROP TABLE "employee_job_preset"`); + await queryRunner.query(`ALTER TABLE "temporary_employee_job_preset" RENAME TO "employee_job_preset"`); + await queryRunner.query(`DROP INDEX "IDX_3e128d30e9910ff920eee4ef37"`); + await queryRunner.query(`DROP INDEX "IDX_c5c4366237dc2bb176c1503426"`); + await queryRunner.query(`DROP INDEX "IDX_75855b44250686f84b7c4bc1f1"`); + await queryRunner.query(`DROP INDEX "IDX_063324fdceb51f7086e401ed2c"`); + await queryRunner.query(`DROP INDEX "IDX_7cf84e8b5775f349f81a1f3cc4"`); + await queryRunner.query(`DROP INDEX "IDX_9d8afc1e1e64d4b7d48dd2229d"`); + await queryRunner.query(`DROP INDEX "IDX_37215da8dee9503d759adb3538"`); + await queryRunner.query(`DROP INDEX "IDX_c210effeb6314d325bc024d21e"`); + await queryRunner.query(`DROP INDEX "IDX_bc1e32c13683dbb16ada1c6da1"`); + await queryRunner.query(`DROP INDEX "IDX_18e22d4b569159bb91dec869aa"`); + await queryRunner.query(`DROP INDEX "IDX_3590135ac2034d7aa88efa7e52"`); + await queryRunner.query(`DROP INDEX "IDX_904ae0b765faef6ba2db8b1e69"`); + await queryRunner.query( + `CREATE TABLE "temporary_organization_project" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "name" varchar NOT NULL, "startDate" datetime, "endDate" datetime, "billing" varchar, "currency" varchar, "public" boolean, "owner" varchar, "taskListType" varchar NOT NULL DEFAULT ('GRID'), "code" varchar, "description" varchar, "color" varchar, "billable" boolean, "billingFlat" boolean, "openSource" boolean, "projectUrl" varchar, "openSourceProjectUrl" varchar, "budget" integer, "budgetType" text DEFAULT ('cost'), "organizationContactId" varchar, "membersCount" integer DEFAULT (0), "imageUrl" varchar(500), "imageId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "repositoryId" varchar, "isTasksAutoSync" boolean DEFAULT (1), "isTasksAutoSyncOnLabel" boolean DEFAULT (1), "syncTag" varchar, "deletedAt" datetime, "fix_relational_custom_fields" boolean, CONSTRAINT "FK_063324fdceb51f7086e401ed2c9" FOREIGN KEY ("imageId") REFERENCES "image_asset" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_7cf84e8b5775f349f81a1f3cc44" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_9d8afc1e1e64d4b7d48dd2229d7" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_bc1e32c13683dbb16ada1c6da14" FOREIGN KEY ("organizationContactId") REFERENCES "organization_contact" ("id") ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT "FK_904ae0b765faef6ba2db8b1e698" FOREIGN KEY ("repositoryId") REFERENCES "organization_github_repository" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_organization_project"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "startDate", "endDate", "billing", "currency", "public", "owner", "taskListType", "code", "description", "color", "billable", "billingFlat", "openSource", "projectUrl", "openSourceProjectUrl", "budget", "budgetType", "organizationContactId", "membersCount", "imageUrl", "imageId", "isActive", "isArchived", "repositoryId", "isTasksAutoSync", "isTasksAutoSyncOnLabel", "syncTag", "deletedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "startDate", "endDate", "billing", "currency", "public", "owner", "taskListType", "code", "description", "color", "billable", "billingFlat", "openSource", "projectUrl", "openSourceProjectUrl", "budget", "budgetType", "organizationContactId", "membersCount", "imageUrl", "imageId", "isActive", "isArchived", "repositoryId", "isTasksAutoSync", "isTasksAutoSyncOnLabel", "syncTag", "deletedAt" FROM "organization_project"` + ); + await queryRunner.query(`DROP TABLE "organization_project"`); + await queryRunner.query(`ALTER TABLE "temporary_organization_project" RENAME TO "organization_project"`); + await queryRunner.query(`CREATE INDEX "IDX_3e128d30e9910ff920eee4ef37" ON "organization_project" ("syncTag") `); + await queryRunner.query( + `CREATE INDEX "IDX_c5c4366237dc2bb176c1503426" ON "organization_project" ("isTasksAutoSyncOnLabel") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_75855b44250686f84b7c4bc1f1" ON "organization_project" ("isTasksAutoSync") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_063324fdceb51f7086e401ed2c" ON "organization_project" ("imageId") `); + await queryRunner.query( + `CREATE INDEX "IDX_7cf84e8b5775f349f81a1f3cc4" ON "organization_project" ("tenantId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_9d8afc1e1e64d4b7d48dd2229d" ON "organization_project" ("organizationId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_37215da8dee9503d759adb3538" ON "organization_project" ("name") `); + await queryRunner.query( + `CREATE INDEX "IDX_c210effeb6314d325bc024d21e" ON "organization_project" ("currency") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_bc1e32c13683dbb16ada1c6da1" ON "organization_project" ("organizationContactId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_18e22d4b569159bb91dec869aa" ON "organization_project" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_3590135ac2034d7aa88efa7e52" ON "organization_project" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_904ae0b765faef6ba2db8b1e69" ON "organization_project" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_68e75e49f06409fd385b4f8774" ON "employee_job_preset" ("employeeId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7ae5b4d4bdec77971dab319f2e" ON "employee_job_preset" ("jobPresetId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_68e75e49f06409fd385b4f8774"`); + await queryRunner.query(`DROP INDEX "IDX_7ae5b4d4bdec77971dab319f2e"`); + await queryRunner.query( + `CREATE TABLE "temporary_employee_job_preset" ("jobPresetId" varchar NOT NULL, "employeeId" varchar NOT NULL, CONSTRAINT "FK_68e75e49f06409fd385b4f87746" FOREIGN KEY ("employeeId") REFERENCES "employee" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_7ae5b4d4bdec77971dab319f2e2" FOREIGN KEY ("jobPresetId") REFERENCES "job_preset" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, PRIMARY KEY ("jobPresetId", "employeeId"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_employee_job_preset"("jobPresetId", "employeeId") SELECT "jobPresetId", "employeeId" FROM "employee_job_preset"` + ); + await queryRunner.query(`DROP TABLE "employee_job_preset"`); + await queryRunner.query(`ALTER TABLE "temporary_employee_job_preset" RENAME TO "employee_job_preset"`); + await queryRunner.query( + `CREATE INDEX "IDX_68e75e49f06409fd385b4f8774" ON "employee_job_preset" ("employeeId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7ae5b4d4bdec77971dab319f2e" ON "employee_job_preset" ("jobPresetId") ` + ); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_7ae5b4d4bdec77971dab319f2e"`); + await queryRunner.query(`DROP INDEX "IDX_68e75e49f06409fd385b4f8774"`); + await queryRunner.query(`ALTER TABLE "employee_job_preset" RENAME TO "temporary_employee_job_preset"`); + await queryRunner.query( + `CREATE TABLE "employee_job_preset" ("jobPresetId" varchar NOT NULL, "employeeId" varchar NOT NULL, CONSTRAINT "FK_68e75e49f06409fd385b4f87746" FOREIGN KEY ("employeeId") REFERENCES "employee" ("id") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY ("jobPresetId", "employeeId"))` + ); + await queryRunner.query( + `INSERT INTO "employee_job_preset"("jobPresetId", "employeeId") SELECT "jobPresetId", "employeeId" FROM "temporary_employee_job_preset"` + ); + await queryRunner.query(`DROP TABLE "temporary_employee_job_preset"`); + await queryRunner.query( + `CREATE INDEX "IDX_7ae5b4d4bdec77971dab319f2e" ON "employee_job_preset" ("jobPresetId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_68e75e49f06409fd385b4f8774" ON "employee_job_preset" ("employeeId") ` + ); + await queryRunner.query(`DROP INDEX "IDX_7ae5b4d4bdec77971dab319f2e"`); + await queryRunner.query(`DROP INDEX "IDX_68e75e49f06409fd385b4f8774"`); + await queryRunner.query(`DROP INDEX "IDX_904ae0b765faef6ba2db8b1e69"`); + await queryRunner.query(`DROP INDEX "IDX_3590135ac2034d7aa88efa7e52"`); + await queryRunner.query(`DROP INDEX "IDX_18e22d4b569159bb91dec869aa"`); + await queryRunner.query(`DROP INDEX "IDX_bc1e32c13683dbb16ada1c6da1"`); + await queryRunner.query(`DROP INDEX "IDX_c210effeb6314d325bc024d21e"`); + await queryRunner.query(`DROP INDEX "IDX_37215da8dee9503d759adb3538"`); + await queryRunner.query(`DROP INDEX "IDX_9d8afc1e1e64d4b7d48dd2229d"`); + await queryRunner.query(`DROP INDEX "IDX_7cf84e8b5775f349f81a1f3cc4"`); + await queryRunner.query(`DROP INDEX "IDX_063324fdceb51f7086e401ed2c"`); + await queryRunner.query(`DROP INDEX "IDX_75855b44250686f84b7c4bc1f1"`); + await queryRunner.query(`DROP INDEX "IDX_c5c4366237dc2bb176c1503426"`); + await queryRunner.query(`DROP INDEX "IDX_3e128d30e9910ff920eee4ef37"`); + await queryRunner.query(`ALTER TABLE "organization_project" RENAME TO "temporary_organization_project"`); + await queryRunner.query( + `CREATE TABLE "organization_project" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "name" varchar NOT NULL, "startDate" datetime, "endDate" datetime, "billing" varchar, "currency" varchar, "public" boolean, "owner" varchar, "taskListType" varchar NOT NULL DEFAULT ('GRID'), "code" varchar, "description" varchar, "color" varchar, "billable" boolean, "billingFlat" boolean, "openSource" boolean, "projectUrl" varchar, "openSourceProjectUrl" varchar, "budget" integer, "budgetType" text DEFAULT ('cost'), "organizationContactId" varchar, "membersCount" integer DEFAULT (0), "imageUrl" varchar(500), "imageId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "repositoryId" varchar, "isTasksAutoSync" boolean DEFAULT (1), "isTasksAutoSyncOnLabel" boolean DEFAULT (1), "syncTag" varchar, "deletedAt" datetime, CONSTRAINT "FK_063324fdceb51f7086e401ed2c9" FOREIGN KEY ("imageId") REFERENCES "image_asset" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_7cf84e8b5775f349f81a1f3cc44" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_9d8afc1e1e64d4b7d48dd2229d7" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_bc1e32c13683dbb16ada1c6da14" FOREIGN KEY ("organizationContactId") REFERENCES "organization_contact" ("id") ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT "FK_904ae0b765faef6ba2db8b1e698" FOREIGN KEY ("repositoryId") REFERENCES "organization_github_repository" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "organization_project"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "startDate", "endDate", "billing", "currency", "public", "owner", "taskListType", "code", "description", "color", "billable", "billingFlat", "openSource", "projectUrl", "openSourceProjectUrl", "budget", "budgetType", "organizationContactId", "membersCount", "imageUrl", "imageId", "isActive", "isArchived", "repositoryId", "isTasksAutoSync", "isTasksAutoSyncOnLabel", "syncTag", "deletedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "startDate", "endDate", "billing", "currency", "public", "owner", "taskListType", "code", "description", "color", "billable", "billingFlat", "openSource", "projectUrl", "openSourceProjectUrl", "budget", "budgetType", "organizationContactId", "membersCount", "imageUrl", "imageId", "isActive", "isArchived", "repositoryId", "isTasksAutoSync", "isTasksAutoSyncOnLabel", "syncTag", "deletedAt" FROM "temporary_organization_project"` + ); + await queryRunner.query(`DROP TABLE "temporary_organization_project"`); + await queryRunner.query( + `CREATE INDEX "IDX_904ae0b765faef6ba2db8b1e69" ON "organization_project" ("repositoryId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_3590135ac2034d7aa88efa7e52" ON "organization_project" ("isArchived") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_18e22d4b569159bb91dec869aa" ON "organization_project" ("isActive") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_bc1e32c13683dbb16ada1c6da1" ON "organization_project" ("organizationContactId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_c210effeb6314d325bc024d21e" ON "organization_project" ("currency") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_37215da8dee9503d759adb3538" ON "organization_project" ("name") `); + await queryRunner.query( + `CREATE INDEX "IDX_9d8afc1e1e64d4b7d48dd2229d" ON "organization_project" ("organizationId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7cf84e8b5775f349f81a1f3cc4" ON "organization_project" ("tenantId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_063324fdceb51f7086e401ed2c" ON "organization_project" ("imageId") `); + await queryRunner.query( + `CREATE INDEX "IDX_75855b44250686f84b7c4bc1f1" ON "organization_project" ("isTasksAutoSync") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_c5c4366237dc2bb176c1503426" ON "organization_project" ("isTasksAutoSyncOnLabel") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_3e128d30e9910ff920eee4ef37" ON "organization_project" ("syncTag") `); + await queryRunner.query(`ALTER TABLE "employee_job_preset" RENAME TO "temporary_employee_job_preset"`); + await queryRunner.query( + `CREATE TABLE "employee_job_preset" ("jobPresetId" varchar NOT NULL, "employeeId" varchar NOT NULL, CONSTRAINT "FK_7ae5b4d4bdec77971dab319f2e2" FOREIGN KEY ("jobPresetId") REFERENCES "job_preset" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_68e75e49f06409fd385b4f87746" FOREIGN KEY ("employeeId") REFERENCES "employee" ("id") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY ("jobPresetId", "employeeId"))` + ); + await queryRunner.query( + `INSERT INTO "employee_job_preset"("jobPresetId", "employeeId") SELECT "jobPresetId", "employeeId" FROM "temporary_employee_job_preset"` + ); + await queryRunner.query(`DROP TABLE "temporary_employee_job_preset"`); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`organization_project\` ADD \`fix_relational_custom_fields\` tinyint NULL` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`organization_project\` DROP COLUMN \`fix_relational_custom_fields\``); + } +} diff --git a/packages/core/src/deal/deal.controller.ts b/packages/core/src/deal/deal.controller.ts index a622bf2c914..9036efa7371 100644 --- a/packages/core/src/deal/deal.controller.ts +++ b/packages/core/src/deal/deal.controller.ts @@ -1,57 +1,82 @@ +import { Body, Controller, Get, HttpStatus, Param, Post, Query, UseGuards } from '@nestjs/common'; import { - Controller, - Get, - HttpStatus, - Param, - Query, - UseGuards -} from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { IPagination } from '@gauzy/contracts'; -import { CrudController } from './../core/crud'; + ApiBadRequestResponse, + ApiCreatedResponse, + ApiInternalServerErrorResponse, + ApiOperation, + ApiResponse, + ApiTags +} from '@nestjs/swagger'; +import { ID, IPagination, PermissionsEnum } from '@gauzy/contracts'; import { Deal } from './deal.entity'; import { DealService } from './deal.service'; -import { TenantPermissionGuard } from './../shared/guards'; -import { ParseJsonPipe, UUIDValidationPipe } from './../shared/pipes'; +import { CrudController, OptionParams, PaginationParams } from '../core/crud'; +import { Permissions } from '../shared/decorators'; +import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; +import { UseValidationPipe, UUIDValidationPipe } from '../shared/pipes'; +import { CreateDealDTO } from './dto'; @ApiTags('Deal') -@UseGuards(TenantPermissionGuard) +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Permissions(PermissionsEnum.VIEW_SALES_PIPELINES) @Controller() export class DealController extends CrudController { - public constructor(private readonly dealService: DealService) { - super(dealService); + constructor(private readonly _dealService: DealService) { + super(_dealService); } - @ApiOperation({ summary: 'Find all deals' }) + /** + * Find all sales pipelines with permissions, API documentation, and query parameter parsing. + * + * @param data - The query parameter data. + * @returns A paginated result of sales pipelines. + */ + @ApiOperation({ summary: 'find all' }) @ApiResponse({ status: HttpStatus.OK, description: 'Found records' }) - @Get() - public async findAll( - @Query('data', ParseJsonPipe) data: any - ): Promise> { - const { relations = [], findInput: where = null } = data; - return this.dealService.findAll({ - relations, - where - }); + @Permissions(PermissionsEnum.VIEW_SALES_PIPELINES) + @Get('/') + async findAll(@Query() filter: PaginationParams): Promise> { + return await this._dealService.findAll(filter); } - @ApiOperation({ summary: 'Find one deal' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Found record' - }) - @Get(':id') - public async getOne( - @Param('id', UUIDValidationPipe) id: string, - @Query('data', ParseJsonPipe) data: any - ): Promise { - const { relations = [], findInput: where = null } = data; - return await this.dealService.findOneByIdString(id, { - relations, - where - }); + /** + * Find a deal by ID. + * + * Retrieves a deal by its unique identifier. + * + * @param id - The ID of the deal to retrieve. + * @param query - Query parameters for relations. + * @returns A promise resolving to the found deal entity. + */ + @Get('/:id') + @ApiOperation({ summary: 'Find a deal by ID' }) + @ApiResponse({ status: 200, description: 'The found deal' }) + @ApiResponse({ status: 404, description: 'Deal not found' }) + async findById(@Param('id', UUIDValidationPipe) id: ID, @Query() options: OptionParams): Promise { + return await this._dealService.findById(id, options); + } + + /** + * Creates a new deal entity. + * + * This method handles the creation of a new deal entity by calling the create method + * on the dealService with the provided entity data. + * + * @param entity - The partial deal entity data to create. + * @returns A promise that resolves to the created deal entity. + */ + @ApiOperation({ summary: 'Create a new deal' }) + @ApiCreatedResponse({ type: Deal, description: 'The deal has been successfully created.' }) + @ApiBadRequestResponse({ description: 'Invalid request data.' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error.' }) + @Permissions(PermissionsEnum.EDIT_SALES_PIPELINES) + @Post('/') + @UseValidationPipe() + async create(@Body() entity: CreateDealDTO): Promise { + // Call the create method on the dealService with the provided entity data + return await this._dealService.create(entity); } } diff --git a/packages/core/src/deal/deal.entity.ts b/packages/core/src/deal/deal.entity.ts index 5904e7df897..0bb17c436c4 100644 --- a/packages/core/src/deal/deal.entity.ts +++ b/packages/core/src/deal/deal.entity.ts @@ -1,34 +1,19 @@ +import { IDeal, IUser, IPipelineStage, IOrganizationContact, ID } from '@gauzy/contracts'; +import { JoinColumn, RelationId } from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, Min, Max, IsInt, IsOptional, IsUUID } from 'class-validator'; +import { OrganizationContact, PipelineStage, TenantOrganizationBaseEntity, User } from '../core/entities/internal'; import { - IDeal, - IUser, - IPipelineStage, - IOrganizationContact -} from '@gauzy/contracts'; -import { - JoinColumn, - RelationId -} from 'typeorm'; -import { ApiProperty } from '@nestjs/swagger'; -import { - IsNotEmpty, - IsString, - Min, - Max, - IsInt, - IsOptional -} from 'class-validator'; -import { - OrganizationContact, - PipelineStage, - TenantOrganizationBaseEntity, - User -} from '../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne, MultiORMOneToOne } from './../core/decorators/entity'; + ColumnIndex, + MultiORMColumn, + MultiORMEntity, + MultiORMManyToOne, + MultiORMOneToOne +} from './../core/decorators/entity'; import { MikroOrmDealRepository } from './repository/mikro-orm-deal.repository'; @MultiORMEntity('deal', { mikroOrmRepository: () => MikroOrmDealRepository }) export class Deal extends TenantOrganizationBaseEntity implements IDeal { - @ApiProperty({ type: () => String }) @IsNotEmpty() @IsString() @@ -36,7 +21,6 @@ export class Deal extends TenantOrganizationBaseEntity implements IDeal { title: string; @ApiProperty({ type: () => Number }) - @IsOptional() @IsInt() @Min(0) @Max(5) @@ -52,36 +36,36 @@ export class Deal extends TenantOrganizationBaseEntity implements IDeal { /** * User */ - @ApiProperty({ type: () => User }) @MultiORMManyToOne(() => User, { - joinColumn: 'createdByUserId', + joinColumn: 'createdByUserId' }) @JoinColumn({ name: 'createdByUserId' }) createdBy: IUser; - @ApiProperty({ type: () => String }) - @RelationId((it: Deal) => it.createdBy) - @IsString() - @IsNotEmpty() + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() @ColumnIndex() + @RelationId((it: Deal) => it.createdBy) @MultiORMColumn({ relationId: true }) - createdByUserId: string; + createdByUserId: ID; /** * PipelineStage */ - @ApiProperty({ type: () => PipelineStage }) - @MultiORMManyToOne(() => PipelineStage, { onDelete: 'CASCADE' }) + @MultiORMManyToOne(() => PipelineStage, { + /** Database cascade action on delete. */ + onDelete: 'CASCADE' + }) @JoinColumn() stage: IPipelineStage; @ApiProperty({ type: () => String }) - @RelationId((it: Deal) => it.stage) - @IsNotEmpty() - @IsString() + @IsUUID() @ColumnIndex() + @RelationId((it: Deal) => it.stage) @MultiORMColumn({ relationId: true }) - stageId: string; + stageId: ID; /* |-------------------------------------------------------------------------- @@ -93,6 +77,9 @@ export class Deal extends TenantOrganizationBaseEntity implements IDeal { * OrganizationContact */ @MultiORMOneToOne(() => OrganizationContact, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true, + /** Database cascade action on delete. */ onDelete: 'CASCADE', @@ -100,12 +87,12 @@ export class Deal extends TenantOrganizationBaseEntity implements IDeal { owner: true }) @JoinColumn() - client: IOrganizationContact; + client?: IOrganizationContact; - @ApiProperty({ type: () => String }) - @RelationId((it: Deal) => it.client) + @ApiPropertyOptional({ type: () => String }) @IsOptional() - @IsString() + @IsUUID() + @RelationId((it: Deal) => it.client) @MultiORMColumn({ nullable: true, relationId: true }) - clientId: string; + clientId?: ID; } diff --git a/packages/core/src/deal/deal.module.ts b/packages/core/src/deal/deal.module.ts index 51bafa79049..e6dccfa32c7 100644 --- a/packages/core/src/deal/deal.module.ts +++ b/packages/core/src/deal/deal.module.ts @@ -10,15 +10,13 @@ import { TypeOrmDealRepository } from './repository/type-orm-deal.repository'; @Module({ imports: [ - RouterModule.register([ - { path: '/deals', module: DealModule } - ]), + RouterModule.register([{ path: '/deals', module: DealModule }]), TypeOrmModule.forFeature([Deal]), MikroOrmModule.forFeature([Deal]), RolePermissionModule ], controllers: [DealController], providers: [DealService, TypeOrmDealRepository], - exports: [DealService, TypeOrmDealRepository] + exports: [TypeOrmModule, MikroOrmModule, DealService, TypeOrmDealRepository] }) -export class DealModule { } +export class DealModule {} diff --git a/packages/core/src/deal/deal.service.ts b/packages/core/src/deal/deal.service.ts index 15cd6813417..098e4cccf05 100644 --- a/packages/core/src/deal/deal.service.ts +++ b/packages/core/src/deal/deal.service.ts @@ -1,4 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { DeepPartial, FindOneOptions } from 'typeorm'; +import { ID } from '@gauzy/contracts'; +import { RequestContext } from '../core/context'; import { TenantAwareCrudService } from './../core/crud'; import { Deal } from './deal.entity'; import { TypeOrmDealRepository } from './repository/type-orm-deal.repository'; @@ -12,4 +15,38 @@ export class DealService extends TenantAwareCrudService { ) { super(typeOrmDealRepository, mikroOrmDealRepository); } + + /** + * Find a Pipeline by ID + * + * @param id - The ID of the Pipeline to find + * @param relations - Optional relations to include in the query + * @returns The found Pipeline + */ + async findById(id: ID, options?: FindOneOptions): Promise { + return await super.findOneByIdString(id, options); + } + + /** + * Creates a new deal entity. + * + * This method sets the `createdByUserId` using the current user's ID from the request context, + * then calls the create method on the superclass (likely a service or repository) with the modified entity data. + * + * @param entity - The partial deal entity data to create. + * @returns A promise that resolves to the created deal entity. + */ + async create(entity: DeepPartial): Promise { + try { + // Set the createdByUserId using the current user's ID from the request context + entity.createdByUserId = RequestContext.currentUserId(); + + // Call the create method on the superclass with the modified entity data + return await super.create(entity); + } catch (error) { + // Handle any errors that occur during deal creation + console.error(`Error occurred while creating deal: ${error.message}`); + throw new HttpException(`Error occurred while creating deal: ${error.message}`, HttpStatus.BAD_REQUEST); + } + } } diff --git a/packages/core/src/deal/dto/create-deal.dto.ts b/packages/core/src/deal/dto/create-deal.dto.ts new file mode 100644 index 00000000000..1fca676b964 --- /dev/null +++ b/packages/core/src/deal/dto/create-deal.dto.ts @@ -0,0 +1,17 @@ +import { IntersectionType, PickType } from '@nestjs/mapped-types'; +import { IDealCreateInput } from '@gauzy/contracts'; +import { Deal } from '../../core/entities/internal'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; + +/** + * Deal DTO + */ +export class DealDTO extends IntersectionType( + TenantOrganizationBaseDTO, + PickType(Deal, ['title', 'probability', 'clientId', 'stageId', 'isActive', 'isArchived']) +) {} + +/** + * Create deal DTO + */ +export class CreateDealDTO extends DealDTO implements IDealCreateInput {} diff --git a/packages/core/src/deal/dto/index.ts b/packages/core/src/deal/dto/index.ts new file mode 100644 index 00000000000..95caa2c3312 --- /dev/null +++ b/packages/core/src/deal/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-deal.dto'; +export * from './update-deal.dto'; diff --git a/packages/core/src/deal/dto/update-deal.dto.ts b/packages/core/src/deal/dto/update-deal.dto.ts new file mode 100644 index 00000000000..1387883d2ba --- /dev/null +++ b/packages/core/src/deal/dto/update-deal.dto.ts @@ -0,0 +1,6 @@ +import { CreateDealDTO } from './create-deal.dto'; + +/** + * Update dea DTO + */ +export class UpdateDealDTO extends CreateDealDTO {} diff --git a/packages/core/src/email-history/commands/handler/email-history.handler.ts b/packages/core/src/email-history/commands/handler/email-history.handler.ts index a933b4bb90d..783c6cbf525 100644 --- a/packages/core/src/email-history/commands/handler/email-history.handler.ts +++ b/packages/core/src/email-history/commands/handler/email-history.handler.ts @@ -1,15 +1,12 @@ -import { IEmailHistory } from '@gauzy/contracts'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { EmailService } from 'email-send/email.service'; import { UpdateResult } from 'typeorm'; +import { IEmailHistory } from '@gauzy/contracts'; +import { EmailService } from '../../../email-send/email.service'; import { EmailHistoryResendCommand } from '../email-history.resend.command'; @CommandHandler(EmailHistoryResendCommand) export class EmailHistoryResendHandler implements ICommandHandler { - - constructor( - private readonly emailService: EmailService - ) { } + constructor(private readonly emailService: EmailService) {} public async execute(command: EmailHistoryResendCommand): Promise { const { input, languageCode } = command; diff --git a/packages/core/src/employee-appointment/employee-appointment.module.ts b/packages/core/src/employee-appointment/employee-appointment.module.ts index d4585ddaee2..5925a5d5935 100644 --- a/packages/core/src/employee-appointment/employee-appointment.module.ts +++ b/packages/core/src/employee-appointment/employee-appointment.module.ts @@ -7,7 +7,7 @@ import { EmployeeAppointment } from './employee-appointment.entity'; import { EmployeeAppointmentController } from './employee-appointment.controller'; import { EmployeeAppointmentService } from './employee-appointment.service'; import { CommandHandlers } from './commands/handlers'; -import { EmailSendModule } from 'email-send/email-send.module'; +import { EmailSendModule } from '../email-send/email-send.module'; import { EmployeeModule } from '../employee/employee.module'; import { OrganizationModule } from '../organization/organization.module'; import { RolePermissionModule } from '../role-permission/role-permission.module'; @@ -27,4 +27,4 @@ import { RolePermissionModule } from '../role-permission/role-permission.module' providers: [EmployeeAppointmentService, ...CommandHandlers], exports: [EmployeeAppointmentService] }) -export class EmployeeAppointmentModule { } +export class EmployeeAppointmentModule {} diff --git a/packages/core/src/employee/commands/get-employee-job-statistics.command.ts b/packages/core/src/employee/commands/get-employee-job-statistics.command.ts deleted file mode 100644 index 06af227e9a6..00000000000 --- a/packages/core/src/employee/commands/get-employee-job-statistics.command.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ICommand } from '@nestjs/cqrs'; -import { PaginationParams } from './../../core/crud'; -import { Employee } from './../employee.entity'; - -export class GetEmployeeJobStatisticsCommand implements ICommand { - static readonly type = '[EmployeeJobStatistics] Get'; - - constructor( - public readonly options: PaginationParams - ) { } -} diff --git a/packages/core/src/employee/commands/handlers/employee.bulk.create.handler.ts b/packages/core/src/employee/commands/handlers/employee.bulk.create.handler.ts index e9ad96999a3..4715f9a1746 100644 --- a/packages/core/src/employee/commands/handlers/employee.bulk.create.handler.ts +++ b/packages/core/src/employee/commands/handlers/employee.bulk.create.handler.ts @@ -5,32 +5,27 @@ import { EmployeeBulkCreateCommand } from '../employee.bulk.create.command'; import { EmployeeCreateCommand } from '../employee.create.command'; @CommandHandler(EmployeeBulkCreateCommand) -export class EmployeeBulkCreateHandler - implements ICommandHandler { +export class EmployeeBulkCreateHandler implements ICommandHandler { + constructor(private readonly commandBus: CommandBus) {} - constructor( - private readonly commandBus: CommandBus - ) {} - - public async execute( - command: EmployeeBulkCreateCommand - ): Promise { + /** + * Executes a bulk create operation for employees. + * @param command The bulk create command containing input, language code, and origin URL. + * @returns A promise that resolves to an array of created employees. + */ + public async execute(command: EmployeeBulkCreateCommand): Promise { try { const { input, languageCode, originUrl } = command; - return await Promise.all( - input.map( - async (entity: IEmployeeCreateInput) => { - return await this.commandBus.execute( - new EmployeeCreateCommand( - entity, - languageCode, - originUrl - ) - ); - } - ) + // Use Promise.all to execute each creation command asynchronously + const results = await Promise.all( + input.map(async (entity: IEmployeeCreateInput) => { + return await this.commandBus.execute(new EmployeeCreateCommand(entity, languageCode, originUrl)); + }) ); + // Return the results array containing created employees + return results; } catch (error) { + // If an error occurs, throw a BadRequestException with the error message throw new BadRequestException(error); } } diff --git a/packages/core/src/employee/commands/handlers/employee.create.handler.ts b/packages/core/src/employee/commands/handlers/employee.create.handler.ts index 50fcd8ab5ce..2c6275d3b4d 100644 --- a/packages/core/src/employee/commands/handlers/employee.create.handler.ts +++ b/packages/core/src/employee/commands/handlers/employee.create.handler.ts @@ -1,10 +1,5 @@ import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { - ComponentLayoutStyleEnum, - IEmployee, - LanguagesEnum, - RolesEnum -} from '@gauzy/contracts'; +import { ComponentLayoutStyleEnum, IEmployee, LanguagesEnum, RolesEnum } from '@gauzy/contracts'; import { environment } from '@gauzy/config'; import { isEmpty } from '@gauzy/common'; import { RequestContext } from './../../../core/context'; @@ -18,9 +13,7 @@ import { RoleService } from './../../../role/role.service'; import { UserService } from './../../../user/user.service'; @CommandHandler(EmployeeCreateCommand) -export class EmployeeCreateHandler - implements ICommandHandler { - +export class EmployeeCreateHandler implements ICommandHandler { constructor( private readonly _commandBus: CommandBus, private readonly _employeeService: EmployeeService, @@ -29,7 +22,7 @@ export class EmployeeCreateHandler private readonly _emailService: EmailService, private readonly _roleService: RoleService, private readonly _userService: UserService - ) { } + ) {} /** * Execute the employee creation command. @@ -39,7 +32,6 @@ export class EmployeeCreateHandler * @throws SomeAppropriateException if an error occurs during the process. */ public async execute(command: EmployeeCreateCommand): Promise { - const { input, originUrl = environment.clientBaseUrl } = command; const languageCode = command.languageCode || LanguagesEnum.ENGLISH; const { organizationId } = input; @@ -69,6 +61,7 @@ export class EmployeeCreateHandler const employee = await this._employeeService.create({ ...input, user, + organizationId, organization: { id: organizationId } }); @@ -88,6 +81,7 @@ export class EmployeeCreateHandler return await this._employeeService.create({ ...input, user, + organizationId, organization: { id: organizationId } }); } catch (error) { diff --git a/packages/core/src/employee/commands/handlers/index.ts b/packages/core/src/employee/commands/handlers/index.ts index 1653632a336..053b6947a36 100644 --- a/packages/core/src/employee/commands/handlers/index.ts +++ b/packages/core/src/employee/commands/handlers/index.ts @@ -2,18 +2,14 @@ import { EmployeeBulkCreateHandler } from './employee.bulk.create.handler'; import { EmployeeCreateHandler } from './employee.create.handler'; import { EmployeeGetHandler } from './employee.get.handler'; import { EmployeeUpdateHandler } from './employee.update.handler'; -import { GetEmployeeJobStatisticsHandler } from './get-employee-job-statistics.handler'; -import { UpdateEmployeeJobSearchStatusHandler } from './update-employee-job-search-status.handler'; import { UpdateEmployeeTotalWorkedHoursHandler } from './update-employee-total-worked-hours.handler'; -import { WorkingEmployeeGetHandler } from './workig-employee.get.handler'; +import { WorkingEmployeeGetHandler } from './working-employee.get.handler'; export const CommandHandlers = [ EmployeeCreateHandler, EmployeeBulkCreateHandler, EmployeeGetHandler, UpdateEmployeeTotalWorkedHoursHandler, - UpdateEmployeeJobSearchStatusHandler, - GetEmployeeJobStatisticsHandler, EmployeeUpdateHandler, WorkingEmployeeGetHandler ]; diff --git a/packages/core/src/employee/commands/handlers/workig-employee.get.handler.ts b/packages/core/src/employee/commands/handlers/working-employee.get.handler.ts similarity index 53% rename from packages/core/src/employee/commands/handlers/workig-employee.get.handler.ts rename to packages/core/src/employee/commands/handlers/working-employee.get.handler.ts index 873ad3932ba..1e3831ebde9 100644 --- a/packages/core/src/employee/commands/handlers/workig-employee.get.handler.ts +++ b/packages/core/src/employee/commands/handlers/working-employee.get.handler.ts @@ -1,25 +1,19 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { IEmployee, IPagination } from '@gauzy/contracts'; import { EmployeeService } from '../../employee.service'; -import { WorkingEmployeeGetCommand } from './../working-employee.get.command'; +import { WorkingEmployeeGetCommand } from '../working-employee.get.command'; @CommandHandler(WorkingEmployeeGetCommand) export class WorkingEmployeeGetHandler implements ICommandHandler { + constructor(private readonly employeeService: EmployeeService) {} - constructor( - private readonly employeeService: EmployeeService - ) {} - - public async execute( - command: WorkingEmployeeGetCommand - ): Promise> { + /** + * + */ + public async execute(command: WorkingEmployeeGetCommand): Promise> { const { input } = command; const { organizationId = null, forRange, withUser } = input; - return await this.employeeService.findWorkingEmployees( - organizationId, - forRange, - withUser - ); + return await this.employeeService.findWorkingEmployees(organizationId, forRange, withUser); } -} \ No newline at end of file +} diff --git a/packages/core/src/employee/commands/index.ts b/packages/core/src/employee/commands/index.ts index 99c7634d9af..8000c8adc46 100644 --- a/packages/core/src/employee/commands/index.ts +++ b/packages/core/src/employee/commands/index.ts @@ -1,8 +1,6 @@ export * from './employee.bulk.create.command'; export * from './employee.create.command'; export * from './employee.get.command'; -export * from './get-employee-job-statistics.command'; -export * from './update-employee-job-search-status.command'; export * from './update-employee-total-worked-hours.command'; export * from './employee.update.command'; -export * from './working-employee.get.command'; \ No newline at end of file +export * from './working-employee.get.command'; diff --git a/packages/core/src/employee/commands/update-employee-job-search-status.command.ts b/packages/core/src/employee/commands/update-employee-job-search-status.command.ts deleted file mode 100644 index 14b426382b8..00000000000 --- a/packages/core/src/employee/commands/update-employee-job-search-status.command.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IEmployee, UpdateEmployeeJobsStatistics } from '@gauzy/contracts'; -import { ICommand } from '@nestjs/cqrs'; - -export class UpdateEmployeeJobSearchStatusCommand implements ICommand { - static readonly type = '[Employee] Update Job Search Status'; - - constructor( - public readonly employeeId: IEmployee['id'], - public readonly input: UpdateEmployeeJobsStatistics - ) { } -} diff --git a/packages/core/src/employee/dto/create-employee.dto.ts b/packages/core/src/employee/dto/create-employee.dto.ts index 07dafb998f5..27347c00930 100644 --- a/packages/core/src/employee/dto/create-employee.dto.ts +++ b/packages/core/src/employee/dto/create-employee.dto.ts @@ -41,4 +41,4 @@ export class CreateEmployeeDTO extends IntersectionType( readonly members?: IEmployee[]; public originalUrl?: string; -} \ No newline at end of file +} diff --git a/packages/core/src/employee/dto/employee-job-statistic.dto.ts b/packages/core/src/employee/dto/employee-job-statistic.dto.ts deleted file mode 100644 index f27bfa7e947..00000000000 --- a/packages/core/src/employee/dto/employee-job-statistic.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsBoolean } from "class-validator"; -import { UpdateEmployeeJobsStatistics } from "@gauzy/contracts"; -import { TenantOrganizationBaseDTO } from "./../../core/dto"; - -/** - * Employee Job Statistic DTO - */ -export class EmployeeJobStatisticDTO extends TenantOrganizationBaseDTO implements UpdateEmployeeJobsStatistics { - - @ApiProperty({ type: () => Boolean }) - @IsBoolean() - isJobSearchActive: boolean; -} diff --git a/packages/core/src/employee/dto/index.ts b/packages/core/src/employee/dto/index.ts index e6f7444bd75..e9c2e99363b 100644 --- a/packages/core/src/employee/dto/index.ts +++ b/packages/core/src/employee/dto/index.ts @@ -7,5 +7,4 @@ export * from './update-employee.dto'; export * from './employee-bulk-input.dto'; export * from './create-employee.dto'; export * from './user-input-dto'; -export * from "./employee-feature.dto"; -export * from "./employee-job-statistic.dto"; +export * from './employee-feature.dto'; diff --git a/packages/core/src/employee/employee-job.controller.ts b/packages/core/src/employee/employee-job.controller.ts deleted file mode 100644 index bc7ffd126f3..00000000000 --- a/packages/core/src/employee/employee-job.controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Body, Controller, Get, HttpStatus, Param, Put, Query, UseGuards } from '@nestjs/common'; -import { CommandBus } from '@nestjs/cqrs'; -import { ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { UpdateResult } from 'typeorm'; -import { IEmployee, IPagination, PermissionsEnum } from '@gauzy/contracts'; -import { Permissions } from '../shared/decorators'; -import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; -import { UUIDValidationPipe, UseValidationPipe } from '../shared/pipes'; -import { PaginationParams } from '../core/crud'; -import { GetEmployeeJobStatisticsCommand, UpdateEmployeeJobSearchStatusCommand } from './commands'; -import { EmployeeJobStatisticDTO } from './dto'; -import { Employee } from './employee.entity'; - -@UseGuards(TenantPermissionGuard, PermissionGuard) -@Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT) -@Controller() -export class EmployeeJobController { - constructor( - private readonly _commandBus: CommandBus - ) { } - - /** - * GET employee job statistics. - * - * This endpoint retrieves statistics related to employee jobs, - * providing details about job distribution, assignments, or other related data. - * - * @param options Pagination parameters for retrieving the data. - * @returns A paginated list of employee job statistics. - */ - @ApiOperation({ summary: 'Retrieve employee job statistics' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Employee job statistics found', - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Invalid input. The response body may contain clues about what went wrong.', - }) - @Permissions(PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW) - @Get('job-statistics') - @UseValidationPipe({ transform: true }) - async getEmployeeJobsStatistics( - @Query() options: PaginationParams - ): Promise> { - return await this._commandBus.execute( - new GetEmployeeJobStatisticsCommand(options) - ); - } - - /** - * UPDATE employee's job search status by their IDs - * - * This endpoint allows updating the job search status of an employee, given their ID. - * - * @param employeeId The unique identifier of the employee whose job search status is being updated. - * @param entity The updated job search status information. - * @returns A promise resolving to the updated employee record or an update result. - */ - @ApiOperation({ summary: 'Update Job Search Status' }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: 'Job search status has been successfully updated.' - }) - @ApiResponse({ - status: HttpStatus.BAD_REQUEST, - description: 'Invalid input. The response body may contain clues as to what went wrong.', - }) - @Put(':id/job-search-status') - @UseValidationPipe({ whitelist: true }) - async updateJobSearchStatus( - @Param('id', UUIDValidationPipe) employeeId: IEmployee['id'], - @Body() data: EmployeeJobStatisticDTO - ): Promise { - return await this._commandBus.execute( - new UpdateEmployeeJobSearchStatusCommand(employeeId, data) - ); - } -} diff --git a/packages/core/src/employee/employee.controller.ts b/packages/core/src/employee/employee.controller.ts index aadc9a0d90e..531605a2c02 100644 --- a/packages/core/src/employee/employee.controller.ts +++ b/packages/core/src/employee/employee.controller.ts @@ -17,7 +17,7 @@ import { CommandBus } from '@nestjs/cqrs'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { DeleteResult } from 'typeorm'; import { I18nLang } from 'nestjs-i18n'; -import { PermissionsEnum, LanguagesEnum, IPagination, IEmployee } from '@gauzy/contracts'; +import { PermissionsEnum, LanguagesEnum, IPagination, IEmployee, ID } from '@gauzy/contracts'; import { EmployeeCreateCommand, EmployeeBulkCreateCommand, @@ -32,12 +32,7 @@ import { BulkBodyLoadTransformPipe, ParseJsonPipe, UUIDValidationPipe, UseValida import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; import { Employee } from './employee.entity'; import { EmployeeService } from './employee.service'; -import { - EmployeeBulkInputDTO, - CreateEmployeeDTO, - UpdateEmployeeDTO, - UpdateProfileDTO -} from './dto'; +import { EmployeeBulkInputDTO, CreateEmployeeDTO, UpdateEmployeeDTO, UpdateProfileDTO } from './dto'; import { RequestContext } from './../core/context'; import { TenantOrganizationBaseDTO } from './../core/dto'; @@ -46,10 +41,7 @@ import { TenantOrganizationBaseDTO } from './../core/dto'; @Permissions(PermissionsEnum.ORG_EMPLOYEES_EDIT) @Controller() export class EmployeeController extends CrudController { - constructor( - private readonly _employeeService: EmployeeService, - private readonly _commandBus: CommandBus - ) { + constructor(private readonly _employeeService: EmployeeService, private readonly _commandBus: CommandBus) { super(_employeeService); } @@ -70,17 +62,13 @@ export class EmployeeController extends CrudController { }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'No working employees found', + description: 'No working employees found' }) @Permissions(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE) @Get('working') - async findAllWorkingEmployees( - @Query('data', ParseJsonPipe) data: any - ): Promise> { + async findAllWorkingEmployees(@Query('data', ParseJsonPipe) data: any): Promise> { const { findInput } = data; - return await this._commandBus.execute( - new WorkingEmployeeGetCommand(findInput) - ); + return await this._commandBus.execute(new WorkingEmployeeGetCommand(findInput)); } /** @@ -96,17 +84,15 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Get the total count of all working employees.' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found the total count of working employees', + description: 'Found the total count of working employees' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Working employees count not found', + description: 'Working employees count not found' }) @Permissions(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE) @Get('working/count') - async findAllWorkingEmployeesCount( - @Query('data', ParseJsonPipe) data: any - ): Promise<{ total: number }> { + async findAllWorkingEmployeesCount(@Query('data', ParseJsonPipe) data: any): Promise<{ total: number }> { const { findInput } = data; const { organizationId, forRange } = findInput; return await this._employeeService.findWorkingEmployeesCount(organizationId, forRange); @@ -127,11 +113,11 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Create multiple employee records in bulk' }) @ApiResponse({ status: HttpStatus.CREATED, - description: 'Records have been successfully created.', + description: 'Records have been successfully created.' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input. The response body may contain clues about what went wrong.', + description: 'Invalid input. The response body may contain clues about what went wrong.' }) @Post('bulk') async createBulk( @@ -158,22 +144,20 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Get employee count in the same tenant' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Successfully retrieved the employee count.', + description: 'Successfully retrieved the employee count.' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid query parameters. Please check your input.', + description: 'Invalid query parameters. Please check your input.' }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, - description: 'An error occurred while retrieving the employee count.', + description: 'An error occurred while retrieving the employee count.' }) @Permissions(PermissionsEnum.ORG_EMPLOYEES_VIEW) @Get('count') @UseValidationPipe() - async getCount( - @Query() options: CountQueryDTO - ): Promise { + async getCount(@Query() options: CountQueryDTO): Promise { return await this._employeeService.countBy(options); } @@ -189,22 +173,20 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Get employees by pagination in the same tenant' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Successfully retrieved paginated employees.', + description: 'Successfully retrieved paginated employees.' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid query parameters. Please check your input.', + description: 'Invalid query parameters. Please check your input.' }) @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, - description: 'An error occurred while retrieving paginated employees.', + description: 'An error occurred while retrieving paginated employees.' }) @Permissions(PermissionsEnum.ORG_EMPLOYEES_VIEW) @Get('pagination') @UseValidationPipe({ transform: true }) - async pagination( - @Query() params: PaginationParams - ): Promise> { + async pagination(@Query() params: PaginationParams): Promise> { return await this._employeeService.pagination(params); } @@ -220,22 +202,20 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Find all employees in the same tenant.' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Successfully found employees in the tenant.', + description: 'Successfully found employees in the tenant.' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'No employees found for the given criteria.', + description: 'No employees found for the given criteria.' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid query parameters. Please check your input.', + description: 'Invalid query parameters. Please check your input.' }) @Permissions(PermissionsEnum.ORG_EMPLOYEES_VIEW) @Get() @UseValidationPipe() - async findAll( - @Query() options: PaginationParams - ): Promise> { + async findAll(@Query() options: PaginationParams): Promise> { // Enforce that only active, non-archived users are retrieved const where = { ...(options.where || {}), @@ -262,16 +242,16 @@ export class EmployeeController extends CrudController { }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Employee record not found.', + description: 'Employee record not found.' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input. Check your query parameters.', + description: 'Invalid input. Check your query parameters.' }) @Permissions() @Get(':id') async findById( - @Param('id', UUIDValidationPipe) id: IEmployee['id'], + @Param('id', UUIDValidationPipe) id: ID, @Query() params: OptionParams ): Promise { const currentEmployeeId = RequestContext.currentEmployeeId(); @@ -279,15 +259,15 @@ export class EmployeeController extends CrudController { // Check permissions to determine the correct ID to retrieve const searchCriteria = { where: { - ...(RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE) ? { id } : { id: currentEmployeeId }) + ...(RequestContext.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE) + ? { id } + : { id: currentEmployeeId }) }, ...(params.relations ? { relations: params.relations } : {}), withDeleted: true }; - return await this._commandBus.execute( - new EmployeeGetCommand(searchCriteria) - ); + return await this._commandBus.execute(new EmployeeGetCommand(searchCriteria)); } /** @@ -317,9 +297,7 @@ export class EmployeeController extends CrudController { @Headers('origin') origin: string, @I18nLang() languageCode: LanguagesEnum ): Promise { - return await this._commandBus.execute( - new EmployeeCreateCommand(entity, languageCode, origin) - ); + return await this._commandBus.execute(new EmployeeCreateCommand(entity, languageCode, origin)); } /** @@ -334,7 +312,7 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Update an existing employee by ID' }) @ApiResponse({ status: HttpStatus.ACCEPTED, - description: 'The employee record has been successfully updated.', + description: 'The employee record has been successfully updated.' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, @@ -347,17 +325,10 @@ export class EmployeeController extends CrudController { @HttpCode(HttpStatus.ACCEPTED) @Put(':id') @UseValidationPipe({ whitelist: true }) - async update( - @Param('id', UUIDValidationPipe) id: IEmployee['id'], - @Body() entity: UpdateEmployeeDTO - ): Promise { - return await this._commandBus.execute( - new EmployeeUpdateCommand(id, entity) - ); + async update(@Param('id', UUIDValidationPipe) id: ID, @Body() entity: UpdateEmployeeDTO): Promise { + return await this._commandBus.execute(new EmployeeUpdateCommand(id, entity)); } - - /** * Update employee's own profile by themselves * @@ -370,22 +341,17 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Update Employee Own Profile' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Profile has been successfully updated.', + description: 'Profile has been successfully updated.' }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, - description: 'Invalid input. Check the response body for more details.', + description: 'Invalid input. Check the response body for more details.' }) @Permissions(PermissionsEnum.PROFILE_EDIT) @Put(':id/profile') @UseValidationPipe({ whitelist: true }) - async updateProfile( - @Param('id', UUIDValidationPipe) id: string, - @Body() entity: UpdateProfileDTO - ): Promise { - return await this._commandBus.execute( - new EmployeeUpdateCommand(id, entity) - ); + async updateProfile(@Param('id', UUIDValidationPipe) id: ID, @Body() entity: UpdateProfileDTO): Promise { + return await this._commandBus.execute(new EmployeeUpdateCommand(id, entity)); } /** @@ -407,10 +373,10 @@ export class EmployeeController extends CrudController { @Delete(':id') @UseValidationPipe({ whitelist: true }) async delete( - @Param('id', UUIDValidationPipe) employeeId: IEmployee['id'], + @Param('id', UUIDValidationPipe) id: ID, @Query() options: TenantOrganizationBaseDTO ): Promise { - return await this._employeeService.delete(employeeId, { where: { ...options } }); + return await this._employeeService.delete(id, { where: { ...options } }); } /** @@ -425,21 +391,21 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Soft delete employee record' }) @ApiResponse({ status: HttpStatus.OK, - description: 'The record has been successfully soft-deleted', + description: 'The record has been successfully soft-deleted' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Employee record not found', + description: 'Employee record not found' }) @HttpCode(HttpStatus.ACCEPTED) @Delete(':id/soft') @UseValidationPipe({ whitelist: true }) async softRemove( - @Param('id', UUIDValidationPipe) employeeId: IEmployee['id'], + @Param('id', UUIDValidationPipe) id: ID, @Query() params: TenantOrganizationBaseDTO ): Promise { // Soft remove the employee by ID - return await this._employeeService.softRemovedById(employeeId, params); + return await this._employeeService.softRemovedById(id, params); } /** @@ -454,20 +420,20 @@ export class EmployeeController extends CrudController { @ApiOperation({ summary: 'Restore a soft-deleted employee record' }) @ApiResponse({ status: HttpStatus.OK, - description: 'The record has been successfully restored', + description: 'The record has been successfully restored' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, - description: 'Employee record not found', + description: 'Employee record not found' }) @HttpCode(HttpStatus.ACCEPTED) @Put(':id/recover') @UseValidationPipe({ whitelist: true }) async softRecover( - @Param('id', UUIDValidationPipe) employeeId: IEmployee['id'], + @Param('id', UUIDValidationPipe) id: ID, @Body() params: TenantOrganizationBaseDTO ): Promise { // Attempt to recover the soft-removed employee - return await this._employeeService.softRecoverById(employeeId, params); + return await this._employeeService.softRecoverById(id, params); } } diff --git a/packages/core/src/employee/employee.module.ts b/packages/core/src/employee/employee.module.ts index 6d1f50c4b70..b72b3cb7c77 100644 --- a/packages/core/src/employee/employee.module.ts +++ b/packages/core/src/employee/employee.module.ts @@ -3,13 +3,11 @@ import { CqrsModule } from '@nestjs/cqrs'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RouterModule } from '@nestjs/core'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { GauzyAIModule } from '@gauzy/integration-ai'; import { TimeLog } from './../core/entities/internal'; import { Employee } from './employee.entity'; import { UserModule } from './../user/user.module'; import { CommandHandlers } from './commands/handlers'; import { EmployeeController } from './employee.controller'; -import { EmployeeJobController } from './employee-job.controller'; import { EmployeeService } from './employee.service'; import { AuthModule } from './../auth/auth.module'; import { EmailSendModule } from './../email-send/email-send.module'; @@ -20,9 +18,7 @@ import { TypeOrmEmployeeRepository } from './repository/type-orm-employee.reposi @Module({ imports: [ - RouterModule.register([ - { path: '/employee', module: EmployeeModule } - ]), + RouterModule.register([{ path: '/employee', module: EmployeeModule }]), TypeOrmModule.forFeature([Employee, TimeLog]), MikroOrmModule.forFeature([Employee, TimeLog]), forwardRef(() => EmailSendModule), @@ -31,11 +27,10 @@ import { TypeOrmEmployeeRepository } from './repository/type-orm-employee.reposi forwardRef(() => UserModule), forwardRef(() => AuthModule), RoleModule, - GauzyAIModule.forRoot(), CqrsModule ], - controllers: [EmployeeJobController, EmployeeController], + controllers: [EmployeeController], providers: [EmployeeService, TypeOrmEmployeeRepository, ...CommandHandlers], exports: [TypeOrmModule, MikroOrmModule, EmployeeService, TypeOrmEmployeeRepository] }) -export class EmployeeModule { } +export class EmployeeModule {} diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index 003ee49d3f6..78287b6ca04 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -446,7 +446,7 @@ export class EmployeeService extends TenantAwareCrudService { // Perform the soft delete operation return await super.softRemove(employeeId, { where: { organizationId, tenantId }, - relations: { user: true, teams: true } + relations: { user: { organizations: true }, teams: true } }); } catch (error) { console.error('Error during soft delete for employee', error); @@ -477,7 +477,7 @@ export class EmployeeService extends TenantAwareCrudService { // Perform the soft recovery operation using the ID, organization ID, and tenant ID return await super.softRecover(employeeId, { where: { organizationId, tenantId }, - relations: { user: true, teams: true }, + relations: { user: { organizations: true }, teams: true }, withDeleted: true }); } catch (error) { diff --git a/packages/core/src/employee/employee.subscriber.ts b/packages/core/src/employee/employee.subscriber.ts index 9d39a755108..8a3de5d03ac 100644 --- a/packages/core/src/employee/employee.subscriber.ts +++ b/packages/core/src/employee/employee.subscriber.ts @@ -63,7 +63,7 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { entity.user.imageUrl = getUserDummyImage(entity.user); } - // + // Updates the employee's status based on the start and end work dates. this.updateEmployeeStatus(entity); } catch (error) { console.error( @@ -80,6 +80,7 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { */ async beforeEntityUpdate(entity: Employee): Promise { try { + // Updates the employee's status based on the start and end work dates. this.updateEmployeeStatus(entity); } catch (error) { console.error( diff --git a/packages/core/src/event-bus/base-entity-event.ts b/packages/core/src/event-bus/base-entity-event.ts index 4f71cc33481..d7e5c6908da 100644 --- a/packages/core/src/event-bus/base-entity-event.ts +++ b/packages/core/src/event-bus/base-entity-event.ts @@ -1,3 +1,4 @@ +import { RequestContext } from '../core/context'; import { BaseEvent } from './base-event'; /** @@ -9,33 +10,33 @@ export type BaseEntityEventType = 'created' | 'updated' | 'deleted'; * Enum representing the possible types of BaseEntity events. */ export enum BaseEntityEventTypeEnum { - CREATED = 'created', - UPDATED = 'updated', - DELETED = 'deleted' + CREATED = 'created', + UPDATED = 'updated', + DELETED = 'deleted' } /** * Abstract class representing a base event for entities with generic types for the entity and input data. */ export abstract class BaseEntityEvent extends BaseEvent { - public readonly entity: Entity; - public readonly type: BaseEntityEventType; - public readonly input?: Input; + public readonly entity: Entity; + public readonly type: BaseEntityEventType; + public readonly ctx: RequestContext; + public readonly input?: Input; - /** - * Constructor for BaseEntityEvent. - * @param entity - The entity associated with the event. - * @param type - The type of the event ('created', 'updated', 'deleted'). - * @param input - Optional input data associated with the event. - */ - protected constructor( - entity: Entity, - type: BaseEntityEventType, - input?: Input, - ) { - super(); - this.entity = entity; - this.type = type; - this.input = input; - } + /** + * Constructor for the BaseEntityEvent class. + * + * @param entity The entity associated with the event. + * @param type The type of event (created, updated, deleted, etc.). + * @param ctx The request context associated with the event. + * @param input Optional input data associated with the event. + */ + protected constructor(entity: Entity, type: BaseEntityEventType, ctx: RequestContext, input?: Input) { + super(); + this.entity = entity; + this.type = type; + this.ctx = ctx; + this.input = input; + } } diff --git a/packages/core/src/event-bus/base-event.ts b/packages/core/src/event-bus/base-event.ts index b8034be01d1..28c7e69964e 100644 --- a/packages/core/src/event-bus/base-event.ts +++ b/packages/core/src/event-bus/base-event.ts @@ -1,17 +1,25 @@ +import { ID } from '@gauzy/contracts'; +import { v4 as uuidv4 } from 'uuid'; + /** * Abstract base class for representing events in an event-driven architecture. */ export abstract class BaseEvent { - /** - * Readonly property representing the creation timestamp of the event. - */ - public readonly createdAt: Date; + /** + * Readonly property representing the unique ID of the event. + */ + public readonly id: ID; + /** + * Readonly property representing the creation timestamp of the event. + */ + public readonly createdAt: Date; - /** - * Constructor for the BaseEvent class. - * Initializes the `createdAt` property with the current date and time. - */ - constructor() { - this.createdAt = new Date(); - } + /** + * Constructor for the BaseEvent class. + * Initializes the `id` with a new UUID and `createdAt` with the current date and time. + */ + constructor() { + this.id = uuidv4(); // Generate a new UUID + this.createdAt = new Date(); + } } diff --git a/packages/core/src/event-bus/events/account-registration-event.ts b/packages/core/src/event-bus/events/account-registration.event.ts similarity index 100% rename from packages/core/src/event-bus/events/account-registration-event.ts rename to packages/core/src/event-bus/events/account-registration.event.ts diff --git a/packages/core/src/event-bus/events/account-verified-event.ts b/packages/core/src/event-bus/events/account-verified.event.ts similarity index 100% rename from packages/core/src/event-bus/events/account-verified-event.ts rename to packages/core/src/event-bus/events/account-verified.event.ts diff --git a/packages/core/src/event-bus/events/handlers/account-registration-handler.ts b/packages/core/src/event-bus/events/handlers/account-registration.handler.ts similarity index 99% rename from packages/core/src/event-bus/events/handlers/account-registration-handler.ts rename to packages/core/src/event-bus/events/handlers/account-registration.handler.ts index c325dffdca4..8ec4b188f1a 100644 --- a/packages/core/src/event-bus/events/handlers/account-registration-handler.ts +++ b/packages/core/src/event-bus/events/handlers/account-registration.handler.ts @@ -1,7 +1,7 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { Subscription, tap } from 'rxjs'; import { EventBus } from '../../event-bus'; -import { AccountRegistrationEvent } from '../account-registration-event'; +import { AccountRegistrationEvent } from '../account-registration.event'; @Injectable() export class AccountRegistrationHandler implements OnModuleInit, OnModuleDestroy { diff --git a/packages/core/src/event-bus/events/handlers/account-verified-handler.ts b/packages/core/src/event-bus/events/handlers/account-verified.handler.ts similarity index 94% rename from packages/core/src/event-bus/events/handlers/account-verified-handler.ts rename to packages/core/src/event-bus/events/handlers/account-verified.handler.ts index f727711852c..1c4cd7a9e8c 100644 --- a/packages/core/src/event-bus/events/handlers/account-verified-handler.ts +++ b/packages/core/src/event-bus/events/handlers/account-verified.handler.ts @@ -1,7 +1,7 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { Subscription, tap } from 'rxjs'; import { EventBus } from '../../event-bus'; -import { AccountVerifiedEvent } from '../account-verified-event'; +import { AccountVerifiedEvent } from '../account-verified.event'; @Injectable() export class AccountVerifiedHandler implements OnModuleInit, OnModuleDestroy { diff --git a/packages/core/src/event-bus/events/handlers/index.ts b/packages/core/src/event-bus/events/handlers/index.ts index a618c9d4c6b..737aab6b9aa 100644 --- a/packages/core/src/event-bus/events/handlers/index.ts +++ b/packages/core/src/event-bus/events/handlers/index.ts @@ -1,4 +1,4 @@ -import { AccountRegistrationHandler } from './account-registration-handler'; -import { AccountVerifiedHandler } from './account-verified-handler'; +import { AccountRegistrationHandler } from './account-registration.handler'; +import { AccountVerifiedHandler } from './account-verified.handler'; export const EventHandlers = [AccountRegistrationHandler, AccountVerifiedHandler]; diff --git a/packages/core/src/event-bus/events/index.ts b/packages/core/src/event-bus/events/index.ts index c70d4635ab2..c878405d7ba 100644 --- a/packages/core/src/event-bus/events/index.ts +++ b/packages/core/src/event-bus/events/index.ts @@ -1,2 +1,4 @@ -export * from './account-registration-event'; -export * from './account-verified-event'; +export * from './account-registration.event'; +export * from './account-verified.event'; +export * from './integration.event'; +export * from './task.event'; diff --git a/packages/core/src/event-bus/events/integration.event.ts b/packages/core/src/event-bus/events/integration.event.ts new file mode 100644 index 00000000000..2a8814801de --- /dev/null +++ b/packages/core/src/event-bus/events/integration.event.ts @@ -0,0 +1,28 @@ +import { IIntegrationTenantCreateInput, IIntegrationTenantUpdateInput } from '@gauzy/contracts'; +import { RequestContext } from '../../core/context'; +import { IntegrationTenant } from '../../core/entities/internal'; +import { BaseEntityEvent, BaseEntityEventType } from '../base-entity-event'; + +type IntegrationInputTypes = IIntegrationTenantCreateInput | IIntegrationTenantUpdateInput; + +/** + * Event class representing an integration events. + */ +export class IntegrationEvent extends BaseEntityEvent { + /** + * Creates an instance of IntegrationEvent. + * + * @param {RequestContext} ctx - The context object containing information about the request. + * @param {IntegrationTenant} entity - The entity associated with the event. + * @param {BaseEntityEventType} type - The type of the event. + * @param {IntegrationInputTypes} [input] - Optional input data for the event. + */ + constructor( + ctx: RequestContext, + entity: IntegrationTenant, + type: BaseEntityEventType, + input?: IntegrationInputTypes + ) { + super(entity, type, ctx, input); + } +} diff --git a/packages/core/src/event-bus/events/task.event.ts b/packages/core/src/event-bus/events/task.event.ts new file mode 100644 index 00000000000..065d58fff25 --- /dev/null +++ b/packages/core/src/event-bus/events/task.event.ts @@ -0,0 +1,23 @@ +import { ITaskCreateInput, ITaskUpdateInput } from '@gauzy/contracts'; +import { RequestContext } from '../../core/context'; +import { Task } from '../../core/entities/internal'; +import { BaseEntityEvent, BaseEntityEventType } from '../base-entity-event'; + +type TaskInputTypes = ITaskCreateInput | ITaskUpdateInput; + +/** + * Event class representing an task events. + */ +export class TaskEvent extends BaseEntityEvent { + /** + * Creates an instance of TaskEvent. + * + * @param {RequestContext} ctx - The context object containing information about the request. + * @param {Task} entity - The task entity associated with the event. + * @param {BaseEntityEventType} type - The type of the event. + * @param {TaskInputTypes} [input] - Optional input data for the event. + */ + constructor(ctx: RequestContext, entity: Task, type: BaseEntityEventType, input?: TaskInputTypes) { + super(entity, type, ctx, input); + } +} diff --git a/packages/core/src/event-bus/index.ts b/packages/core/src/event-bus/index.ts index ddf1bfee613..20fd174518e 100644 --- a/packages/core/src/event-bus/index.ts +++ b/packages/core/src/event-bus/index.ts @@ -1,4 +1,4 @@ export * from './events'; - export * from './event-bus'; export * from './event-bus.module'; +export * from './base-entity-event'; diff --git a/packages/core/src/expense/dto/create-expense.dto.ts b/packages/core/src/expense/dto/create-expense.dto.ts index 1e91677dae8..e7d2ca0642d 100644 --- a/packages/core/src/expense/dto/create-expense.dto.ts +++ b/packages/core/src/expense/dto/create-expense.dto.ts @@ -1,18 +1,20 @@ -import { IExpenseCreateInput } from "@gauzy/contracts"; -import { IntersectionType } from "@nestjs/mapped-types"; -import { PartialType } from "@nestjs/swagger"; -import { RelationalTagDTO } from "./../../tags/dto"; -import { EmployeeFeatureDTO } from "./../../employee/dto"; -import { OrganizationVendorFeatureDTO } from "./../../organization-vendor/dto"; -import { ExpenseDTO } from "./expense.dto"; -import { RelationalCurrencyDTO } from "currency/dto"; +import { IExpenseCreateInput } from '@gauzy/contracts'; +import { IntersectionType } from '@nestjs/mapped-types'; +import { PartialType } from '@nestjs/swagger'; +import { RelationalTagDTO } from './../../tags/dto'; +import { EmployeeFeatureDTO } from './../../employee/dto'; +import { RelationalCurrencyDTO } from '../../currency/dto'; +import { OrganizationVendorFeatureDTO } from './../../organization-vendor/dto'; +import { ExpenseDTO } from './expense.dto'; /** * Create Expense DTO request validation */ -export class CreateExpenseDTO extends IntersectionType( - ExpenseDTO, - OrganizationVendorFeatureDTO, - PartialType(EmployeeFeatureDTO), - IntersectionType(RelationalTagDTO, RelationalCurrencyDTO) -) implements IExpenseCreateInput {} \ No newline at end of file +export class CreateExpenseDTO + extends IntersectionType( + ExpenseDTO, + OrganizationVendorFeatureDTO, + PartialType(EmployeeFeatureDTO), + IntersectionType(RelationalTagDTO, RelationalCurrencyDTO) + ) + implements IExpenseCreateInput {} diff --git a/packages/core/src/expense/dto/update-expense.dto.ts b/packages/core/src/expense/dto/update-expense.dto.ts index 143c0bf0472..1c772e53b54 100644 --- a/packages/core/src/expense/dto/update-expense.dto.ts +++ b/packages/core/src/expense/dto/update-expense.dto.ts @@ -1,15 +1,17 @@ -import { IExpenseUpdateInput } from "@gauzy/contracts"; -import { IntersectionType } from "@nestjs/mapped-types"; -import { RelationalCurrencyDTO } from "./../../currency/dto"; -import { OrganizationVendorFeatureDTO } from "organization-vendor/dto"; -import { RelationalTagDTO } from "./../../tags/dto"; -import { ExpenseDTO } from "./expense.dto"; +import { IExpenseUpdateInput } from '@gauzy/contracts'; +import { IntersectionType } from '@nestjs/mapped-types'; +import { RelationalCurrencyDTO } from './../../currency/dto'; +import { OrganizationVendorFeatureDTO } from '../../organization-vendor/dto'; +import { RelationalTagDTO } from './../../tags/dto'; +import { ExpenseDTO } from './expense.dto'; /** * Update Expense DTO request validation */ -export class UpdateExpenseDTO extends IntersectionType( - ExpenseDTO, - OrganizationVendorFeatureDTO, - IntersectionType(RelationalTagDTO, RelationalCurrencyDTO) -) implements IExpenseUpdateInput {} \ No newline at end of file +export class UpdateExpenseDTO + extends IntersectionType( + ExpenseDTO, + OrganizationVendorFeatureDTO, + IntersectionType(RelationalTagDTO, RelationalCurrencyDTO) + ) + implements IExpenseUpdateInput {} diff --git a/packages/core/src/feature/commands/handlers/feature-toggle.update.handler.ts b/packages/core/src/feature/commands/handlers/feature-toggle.update.handler.ts index f13553f9e9f..f2994854eef 100644 --- a/packages/core/src/feature/commands/handlers/feature-toggle.update.handler.ts +++ b/packages/core/src/feature/commands/handlers/feature-toggle.update.handler.ts @@ -1,16 +1,12 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { FeatureOrganizationService } from 'feature/feature-organization.service'; +import { FeatureOrganizationService } from '../../../feature/feature-organization.service'; import { FeatureToggleUpdateCommand } from '../feature-toggle.update.command'; @CommandHandler(FeatureToggleUpdateCommand) export class FeatureToggleUpdateHandler implements ICommandHandler { - constructor( - private readonly _featureOrganizationService: FeatureOrganizationService - ) {} + constructor(private readonly _featureOrganizationService: FeatureOrganizationService) {} - public async execute( - command: FeatureToggleUpdateCommand - ): Promise { + public async execute(command: FeatureToggleUpdateCommand): Promise { const { input } = command; return await this._featureOrganizationService.updateFeatureOrganization(input); } diff --git a/packages/core/src/health/health.controller.ts b/packages/core/src/health/health.controller.ts index dfc78f291f1..1d47acbdf9f 100644 --- a/packages/core/src/health/health.controller.ts +++ b/packages/core/src/health/health.controller.ts @@ -1,4 +1,3 @@ -import { Public } from '@gauzy/common'; import { Controller, Get } from '@nestjs/common'; import { HealthCheckService, @@ -6,16 +5,16 @@ import { DiskHealthIndicator, MikroOrmHealthIndicator } from '@nestjs/terminus'; -import { CacheHealthIndicator } from './indicators/cache-health.indicator'; -import { RedisHealthIndicator } from './indicators/redis-health.indicator'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { v4 as uuid } from 'uuid'; import * as path from 'path'; import { DataSource, QueryRunner } from 'typeorm'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; -import { getORMType, MultiORM, MultiORMEnum } from 'core/utils'; -import { User } from 'user/user.entity'; -import { TypeOrmUserRepository } from 'user/repository/type-orm-user.repository'; -import { MikroOrmUserRepository } from 'user/repository/mikro-orm-user.repository'; +import { Public } from '@gauzy/common'; +import { User } from '../core/entities/internal'; +import { CacheHealthIndicator } from './indicators/cache-health.indicator'; +import { RedisHealthIndicator } from './indicators/redis-health.indicator'; +import { getORMType, MultiORM, MultiORMEnum } from '../core/utils'; +import { MikroOrmUserRepository, TypeOrmUserRepository } from '../user/repository'; @Controller('health') export class HealthController { @@ -36,7 +35,6 @@ export class HealthController { } private readonly ormType: MultiORM; - private readonly checkDb = true; private readonly checkStorage = true; private readonly checkCache = true; diff --git a/packages/core/src/health/health.module.ts b/packages/core/src/health/health.module.ts index fe5971a9b4e..108532ca09c 100644 --- a/packages/core/src/health/health.module.ts +++ b/packages/core/src/health/health.module.ts @@ -1,12 +1,12 @@ import { ConsoleLogger, Module } from '@nestjs/common'; import { TerminusModule } from '@nestjs/terminus'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { User } from '../core/entities/internal'; import { CacheHealthIndicator } from './indicators/cache-health.indicator'; import { RedisHealthIndicator } from './indicators/redis-health.indicator'; import { HealthController } from './health.controller'; -import { DatabaseModule } from 'database/database.module'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { User } from 'user/user.entity'; +import { DatabaseModule } from '../database/database.module'; @Module({ controllers: [HealthController], diff --git a/packages/core/src/image-asset/image-asset.controller.ts b/packages/core/src/image-asset/image-asset.controller.ts index baffdb616fd..7af28ecb660 100644 --- a/packages/core/src/image-asset/image-asset.controller.ts +++ b/packages/core/src/image-asset/image-asset.controller.ts @@ -4,6 +4,7 @@ import { Param, Post, Body, + Headers, UseGuards, Query, HttpCode, @@ -68,7 +69,11 @@ export class ImageAssetController extends CrudController { }) ) @UseValidationPipe({ whitelist: true }) - async upload(@UploadedFileStorage() file, @Body() entity: UploadImageAsset) { + async upload( + @UploadedFileStorage() file, + @Headers() headers: Record, + @Body() entity: UploadImageAsset + ) { const provider = new FileStorage().getProvider(); let thumbnail: UploadedFile; @@ -106,9 +111,15 @@ export class ImageAssetController extends CrudController { console.error('Error while uploading media asset into file storage provider:', error); } + // Extract tenant and organization IDs from request headers and body + const tenantId = headers['tenant-id'] || entity?.tenantId; + const organizationId = headers['organization-id'] || entity?.organizationId; + return await this._commandBus.execute( new ImageAssetCreateCommand({ ...entity, + tenantId, + organizationId, name: file.filename, url: file.key, thumb: thumbnail ? thumbnail.key : null, diff --git a/packages/core/src/image-asset/image-asset.subscriber.ts b/packages/core/src/image-asset/image-asset.subscriber.ts index 14ccad9d043..e97f7328426 100644 --- a/packages/core/src/image-asset/image-asset.subscriber.ts +++ b/packages/core/src/image-asset/image-asset.subscriber.ts @@ -1,41 +1,37 @@ -import { EventSubscriber } from "typeorm"; -import { FileStorage } from "./../core/file-storage"; -import { BaseEntityEventSubscriber } from "../core/entities/subscribers/base-entity-event.subscriber"; -import { ImageAsset } from "./image-asset.entity"; +import { EventSubscriber } from 'typeorm'; +import { FileStorage } from './../core/file-storage'; +import { BaseEntityEventSubscriber } from '../core/entities/subscribers/base-entity-event.subscriber'; +import { ImageAsset } from './image-asset.entity'; @EventSubscriber() export class ImageAssetSubscriber extends BaseEntityEventSubscriber { + /** + * Indicates that this subscriber only listen to ImageAsset events. + */ + listenTo() { + return ImageAsset; + } - /** - * Indicates that this subscriber only listen to ImageAsset events. - */ - listenTo() { - return ImageAsset; - } + /** + * Called after an ImageAsset entity is loaded from the database. + * This method updates the entity by setting the full and thumbnail URLs using the provided storage provider. + * + * @param entity The ImageAsset entity that has been loaded. + * @returns {Promise} A promise that resolves when the URL updating process is complete. + */ + async afterEntityLoad(entity: ImageAsset): Promise { + try { + if (entity instanceof ImageAsset) { + const { storageProvider, url, thumb } = entity; + const store = new FileStorage().setProvider(storageProvider).getProviderInstance(); - /** - * Called after an ImageAsset entity is loaded from the database. - * This method updates the entity by setting the full and thumbnail URLs using the provided storage provider. - * - * @param entity The ImageAsset entity that has been loaded. - * @returns {Promise} A promise that resolves when the URL updating process is complete. - */ - async afterEntityLoad(entity: ImageAsset): Promise { - try { - if (entity instanceof ImageAsset) { - const { storageProvider, url, thumb } = entity; - const store = new FileStorage().setProvider(storageProvider).getProviderInstance(); - - // Retrieve full and thumbnail URLs concurrently - const [fullUrl, thumbUrl] = await Promise.all([ - store.url(url), - store.url(thumb) - ]); - entity.fullUrl = fullUrl; - entity.thumbUrl = thumbUrl; - } - } catch (error) { - console.error('ImageAssetSubscriber: Error during the afterEntityLoad process:', error); - } - } + // Retrieve full and thumbnail URLs concurrently + const [fullUrl, thumbUrl] = await Promise.all([store.url(url), store.url(thumb)]); + entity.fullUrl = fullUrl; + entity.thumbUrl = thumbUrl; + } + } catch (error) { + console.error('ImageAssetSubscriber: Error during the afterEntityLoad process:', error); + } + } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6cbeb5a677a..14dc17c8e89 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,11 +1,19 @@ export { bootstrap } from './bootstrap'; - +export { + createMigration, + ConnectionEntityManager, + generateMigration, + prepareSQLQuery, + revertLastDatabaseMigration, + runDatabaseMigrations +} from './database'; export * from './logger'; export * from './core'; export * from './core/seeds'; export * from './shared'; -export * from './tenant'; +export * from './event-bus'; +export * from './tenant'; export { RoleModule, RoleService } from './role'; export { RolePermissionModule, RolePermissionService } from './role-permission'; export { UserModule, UserService } from './user'; @@ -29,14 +37,29 @@ export { } from './organization-project'; export * from './employee'; +export { TaskModule, TaskService, TaskCreateCommand, TaskUpdateCommand, AutomationTaskSyncCommand } from './tasks'; +export { IntegrationModule, IntegrationService } from './integration'; export { IntegrationTenantModule, IntegrationTenantService, IntegrationTenantGetCommand, IntegrationTenantUpdateOrCreateCommand } from './integration-tenant'; -export { IntegrationMapModule, IntegrationMapService, IntegrationMapSyncEntityCommand } from './integration-map'; +export { + IntegrationMapModule, + IntegrationMapService, + IntegrationMapSyncActivityCommand, + IntegrationMapSyncEntityCommand, + IntegrationMapSyncOrganizationCommand, + IntegrationMapSyncProjectCommand, + IntegrationMapSyncScreenshotCommand, + IntegrationMapSyncTaskCommand, + IntegrationMapSyncTimeLogCommand, + IntegrationMapSyncTimeSlotCommand, + IntegrationMapSyncIssueCommand, + IntegrationMapSyncLabelCommand +} from './integration-map'; export { IntegrationSettingModule, IntegrationSettingService, @@ -44,14 +67,17 @@ export { IntegrationSettingGetCommand, IntegrationSettingGetManyCommand } from './integration-setting'; - -export { IncomeModule, IncomeService, IncomeCreateCommand } from './income'; -export { ExpenseModule, ExpenseService, ExpenseCreateCommand } from './expense'; export { - ExpenseCategoriesModule, - ExpenseCategoriesService, - ExpenseCategoryFirstOrCreateCommand -} from './expense-categories'; + IntegrationEntitySettingModule, + IntegrationEntitySettingService, + DEFAULT_ENTITY_SETTINGS +} from './integration-entity-setting'; +export { + IntegrationEntitySettingTiedModule, + IntegrationEntitySettingTiedService, + PROJECT_TIED_ENTITIES +} from './integration-entity-setting-tied'; + export { TimeSlotModule, TimeSlotService, @@ -61,5 +87,11 @@ export { export { TimeLogModule, TimeLogService, TimeLogCreateCommand } from './time-tracking/time-log'; export { ScreenshotModule, ScreenshotService, ScreenshotCreateCommand } from './time-tracking/screenshot'; -export * from './tags'; -export * from './database'; +export { IncomeModule, IncomeService, IncomeCreateCommand } from './income'; +export { ExpenseModule, ExpenseService, ExpenseCreateCommand } from './expense'; +export { + ExpenseCategoriesModule, + ExpenseCategoriesService, + ExpenseCategoryFirstOrCreateCommand +} from './expense-categories'; +export { TagModule, TagService, Taggable, AutomationLabelSyncCommand, RelationalTagDTO } from './tags'; diff --git a/packages/core/src/integration-entity-setting-tied/index.ts b/packages/core/src/integration-entity-setting-tied/index.ts new file mode 100644 index 00000000000..af3f06058af --- /dev/null +++ b/packages/core/src/integration-entity-setting-tied/index.ts @@ -0,0 +1,3 @@ +export * from './integration-entity-setting-tied.module'; +export * from './integration-entity-setting-tied.service'; +export * from './integration-entity-setting-tied'; diff --git a/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.module.ts b/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.module.ts index 5148ba900bf..d90e0fce94d 100644 --- a/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.module.ts +++ b/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.module.ts @@ -25,4 +25,4 @@ import { RolePermissionModule } from '../role-permission/role-permission.module' providers: [IntegrationEntitySettingTiedService], exports: [TypeOrmModule, MikroOrmModule, IntegrationEntitySettingTiedService] }) -export class IntegrationEntitySettingTiedModule { } +export class IntegrationEntitySettingTiedModule {} diff --git a/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.seed.ts b/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.seed.ts index 1a4bf478995..55b193d9a1a 100644 --- a/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.seed.ts +++ b/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.seed.ts @@ -1,18 +1,16 @@ import { DataSource } from 'typeorm'; import { faker } from '@faker-js/faker'; import { IIntegrationEntitySettingTied, IntegrationEntity, ITenant } from '@gauzy/contracts'; -import { PROJECT_TIED_ENTITIES } from '@gauzy/integration-hubstaff'; import { IntegrationEntitySetting, IntegrationTenant, Organization } from './../core/entities/internal'; import { IntegrationEntitySettingTied } from './integration-entity-setting-tied.entity'; +import { PROJECT_TIED_ENTITIES } from './integration-entity-setting-tied'; export const createRandomIntegrationEntitySettingTied = async ( dataSource: DataSource, tenants: ITenant[] ): Promise => { if (!tenants) { - console.warn( - 'Warning: tenants not found, Integration Entity Setting will not be created' - ); + console.warn('Warning: tenants not found, Integration Entity Setting will not be created'); return; } @@ -35,26 +33,18 @@ export const createRandomIntegrationEntitySettingTied = async ( integrationEntitySettingTiedEntity.integrationEntitySetting = integrationEntitySetting; integrationEntitySettingTiedEntity.sync = faker.datatype.boolean(); - integrationEntitySettingTiedEntity.organization = faker.helpers.arrayElement( - organizations - ); + integrationEntitySettingTiedEntity.organization = faker.helpers.arrayElement(organizations); integrationEntitySettingTiedEntity.tenant = tenant; //todo: need to understand real values here - if ( - integrationEntitySetting['entity'] === - IntegrationEntity.PROJECT - ) { - integrationEntitySettingTiedEntity.entity = faker.helpers.arrayElement( - PROJECT_TIED_ENTITIES - )['entity']; + if (integrationEntitySetting['entity'] === IntegrationEntity.PROJECT) { + integrationEntitySettingTiedEntity.entity = + faker.helpers.arrayElement(PROJECT_TIED_ENTITIES)['entity']; } else { integrationEntitySettingTiedEntity.entity = faker.helpers.arrayElement( Object.values(IntegrationEntity) ); } - randomIntegrationEntitySettingsTiedEntity.push( - integrationEntitySettingTiedEntity - ); + randomIntegrationEntitySettingsTiedEntity.push(integrationEntitySettingTiedEntity); } } } diff --git a/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.ts b/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.ts new file mode 100644 index 00000000000..14f02f1dd16 --- /dev/null +++ b/packages/core/src/integration-entity-setting-tied/integration-entity-setting-tied.ts @@ -0,0 +1,27 @@ +import { IntegrationEntity } from '@gauzy/contracts'; + +/** + * Interface for entity synchronization settings. + */ +interface IEntitySyncSetting { + entity: IntegrationEntity; + sync: boolean; +} + +/** + * Project-tied entities that need to be synchronized. + */ +export const PROJECT_TIED_ENTITIES: IEntitySyncSetting[] = [ + { + entity: IntegrationEntity.TASK, + sync: true + }, + { + entity: IntegrationEntity.ACTIVITY, + sync: true + }, + { + entity: IntegrationEntity.SCREENSHOT, + sync: true + } +]; diff --git a/packages/core/src/integration-entity-setting/index.ts b/packages/core/src/integration-entity-setting/index.ts new file mode 100644 index 00000000000..e8880c37581 --- /dev/null +++ b/packages/core/src/integration-entity-setting/index.ts @@ -0,0 +1,3 @@ +export * from './integration-entity-setting.module'; +export * from './integration-entity-setting.service'; +export * from './integration-entity-settings'; 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 7da5df5eadb..d4bc09394a0 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 @@ -1,9 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { - - JoinColumn, - RelationId -} from 'typeorm'; +import { JoinColumn, RelationId } from 'typeorm'; import { IsBoolean, IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; import { IIntegrationEntitySetting, @@ -16,12 +12,17 @@ import { IntegrationTenant, TenantOrganizationBaseEntity } from '../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne, MultiORMOneToMany } from './../core/decorators/entity'; +import { + ColumnIndex, + MultiORMColumn, + MultiORMEntity, + MultiORMManyToOne, + MultiORMOneToMany +} from './../core/decorators/entity'; import { MikroOrmIntegrationEntitySettingRepository } from './repository/mikro-orm-integration-entity-setting.repository'; @MultiORMEntity('integration_entity_setting', { mikroOrmRepository: () => MikroOrmIntegrationEntitySettingRepository }) export class IntegrationEntitySetting extends TenantOrganizationBaseEntity implements IIntegrationEntitySetting { - @ApiProperty({ type: () => String, enum: IntegrationEntity }) @IsNotEmpty() @IsEnum(IntegrationEntity) @@ -46,7 +47,7 @@ export class IntegrationEntitySetting extends TenantOrganizationBaseEntity imple @ApiPropertyOptional({ type: () => IntegrationTenant }) @MultiORMManyToOne(() => IntegrationTenant, (it) => it.entitySettings, { /** Database cascade action on delete. */ - onDelete: 'CASCADE', + onDelete: 'CASCADE' }) @JoinColumn() integration?: IIntegrationTenant; diff --git a/packages/core/src/integration-entity-setting/integration-entity-setting.seed.ts b/packages/core/src/integration-entity-setting/integration-entity-setting.seed.ts index daf292536c7..dc0c194f635 100644 --- a/packages/core/src/integration-entity-setting/integration-entity-setting.seed.ts +++ b/packages/core/src/integration-entity-setting/integration-entity-setting.seed.ts @@ -1,46 +1,41 @@ import { DataSource } from 'typeorm'; import { faker } from '@faker-js/faker'; -import { DEFAULT_ENTITY_SETTINGS } from '@gauzy/integration-hubstaff'; import { IIntegrationEntitySetting, IIntegrationEntitySettingTied, ITenant } from '@gauzy/contracts'; -import { IntegrationEntitySetting } from './integration-entity-setting.entity'; import { IntegrationTenant, Organization } from './../core/entities/internal'; +import { IntegrationEntitySetting } from './integration-entity-setting.entity'; +import { DEFAULT_ENTITY_SETTINGS } from './integration-entity-settings'; +/** + * + * @param dataSource + * @param tenants + * @returns + */ export const createRandomIntegrationEntitySetting = async ( dataSource: DataSource, tenants: ITenant[] ): Promise => { if (!tenants) { - console.warn( - 'Warning: tenants not found, Integration Entity Setting will not be created.' - ); + console.warn('Warning: tenants not found, Integration Entity Setting will not be created.'); return; } const integrationEntitySettings: IIntegrationEntitySetting[] = []; const integrationEntitySettingTiedEntities: IIntegrationEntitySettingTied[] = []; - for (const tenant of tenants) { + for await (const tenant of tenants) { const { id: tenantId } = tenant; - const organizations = await dataSource.manager.findBy(Organization, { - tenantId - }); - const integrationTenants = await dataSource.manager.findBy(IntegrationTenant, { - tenantId - }); + const organizations = await dataSource.manager.findBy(Organization, { tenantId }); + const integrationTenants = await dataSource.manager.findBy(IntegrationTenant, { tenantId }); + for (const integrationTenant of integrationTenants) { const integrationEntitySetting = new IntegrationEntitySetting(); - integrationEntitySetting.integration = integrationTenant; integrationEntitySetting.tiedEntities = integrationEntitySettingTiedEntities; integrationEntitySetting.sync = faker.datatype.boolean(); - (integrationEntitySetting.organization = faker.helpers.arrayElement( - organizations - )), - (integrationEntitySetting.tenant = tenant); - //todo: need to understand real values here - integrationEntitySetting.entity = faker.helpers.arrayElement( - DEFAULT_ENTITY_SETTINGS - )['entity']; + integrationEntitySetting.organization = faker.helpers.arrayElement(organizations); + integrationEntitySetting.tenant = tenant; + integrationEntitySetting.entity = faker.helpers.arrayElement(DEFAULT_ENTITY_SETTINGS)['entity']; integrationEntitySettings.push(integrationEntitySetting); } } diff --git a/packages/core/src/integration-entity-setting/integration-entity-settings.ts b/packages/core/src/integration-entity-setting/integration-entity-settings.ts new file mode 100644 index 00000000000..d69e73d4d04 --- /dev/null +++ b/packages/core/src/integration-entity-setting/integration-entity-settings.ts @@ -0,0 +1,27 @@ +import { IntegrationEntity } from '@gauzy/contracts'; + +/** + * Interface for entity synchronization settings. + */ +interface IEntitySyncSetting { + entity: IntegrationEntity; + sync: boolean; +} + +/** + * Default settings for entities to be synchronized. + */ +export const DEFAULT_ENTITY_SETTINGS: IEntitySyncSetting[] = [ + { + entity: IntegrationEntity.ORGANIZATION, + sync: true + }, + { + entity: IntegrationEntity.PROJECT, + sync: true + }, + { + entity: IntegrationEntity.CLIENT, + sync: true + } +]; diff --git a/packages/core/src/integration-map/commands/handlers/integration-map.sync-issue.handler.ts b/packages/core/src/integration-map/commands/handlers/integration-map.sync-issue.handler.ts index 0d4c9d9b167..6714feb9b07 100644 --- a/packages/core/src/integration-map/commands/handlers/integration-map.sync-issue.handler.ts +++ b/packages/core/src/integration-map/commands/handlers/integration-map.sync-issue.handler.ts @@ -1,20 +1,19 @@ import { CommandHandler, ICommandHandler, CommandBus } from '@nestjs/cqrs'; import { IIntegrationMap, IntegrationEntity } from '@gauzy/contracts'; -import { RequestContext } from 'core/context'; -import { TaskService } from 'tasks/task.service'; -import { TaskCreateCommand, TaskUpdateCommand } from 'tasks/commands'; +import { RequestContext } from '../../../core/context'; +import { TaskService } from '../../../tasks/task.service'; +import { TaskCreateCommand, TaskUpdateCommand } from '../../../tasks/commands'; import { IntegrationMapSyncEntityCommand } from './../integration-map.sync-entity.command'; import { IntegrationMapSyncIssueCommand } from './../integration-map.sync-issue.command'; import { IntegrationMapService } from '../../integration-map.service'; @CommandHandler(IntegrationMapSyncIssueCommand) export class IntegrationMapSyncIssueHandler implements ICommandHandler { - constructor( private readonly _commandBus: CommandBus, private readonly _integrationMapService: IntegrationMapService, - private readonly _taskService: TaskService, - ) { } + private readonly _taskService: TaskService + ) {} /** * Execute the IntegrationMapSyncIssueCommand to sync GitHub issues and update tasks. @@ -41,9 +40,7 @@ export class IntegrationMapSyncIssueHandler implements ICommandHandler { - constructor( private readonly _commandBus: CommandBus, private readonly _integrationMapService: IntegrationMapService, - private readonly _tagService: TagService, - ) { } + private readonly _tagService: TagService + ) {} /** * Execute the IntegrationMapSyncLabelCommand to sync GitHub labels and update tags. @@ -41,9 +40,7 @@ export class IntegrationMapSyncLabelHandler implements ICommandHandler { - +export class IntegrationMapSyncTaskHandler implements ICommandHandler { constructor( private readonly _commandBus: CommandBus, private readonly _integrationMapService: IntegrationMapService, private readonly _taskService: TaskService - ) { } + ) {} /** * Third party project task integrated and mapped @@ -23,9 +21,7 @@ export class IntegrationMapSyncTaskHandler * @param command * @returns */ - public async execute( - command: IntegrationMapSyncTaskCommand - ) { + public async execute(command: IntegrationMapSyncTaskCommand) { const { triggeredEvent, input } = command; const { sourceId, organizationId, integrationId, entity } = input; const tenantId = RequestContext.currentTenantId() || input.tenantId; @@ -45,9 +41,7 @@ export class IntegrationMapSyncTaskHandler await this._taskService.findOneByIdString(integrationMap.gauzyId); // Update the corresponding task with the new input data - await this._commandBus.execute( - new TaskUpdateCommand(integrationMap.gauzyId, entity, triggeredEvent) - ); + await this._commandBus.execute(new TaskUpdateCommand(integrationMap.gauzyId, entity, triggeredEvent)); } catch (error) { // Create a corresponding task with the new input data await this._commandBus.execute( @@ -62,9 +56,7 @@ export class IntegrationMapSyncTaskHandler } catch (error) { // Handle errors and create a new task // Create a new task with the provided entity data - const task = await this._commandBus.execute( - new TaskCreateCommand(entity, triggeredEvent) - ); + const task = await this._commandBus.execute(new TaskCreateCommand(entity, triggeredEvent)); // Create a new integration map for the issue return await this._commandBus.execute( diff --git a/packages/core/src/integration-map/index.ts b/packages/core/src/integration-map/index.ts index fc4eda47c6b..3ef992f686c 100644 --- a/packages/core/src/integration-map/index.ts +++ b/packages/core/src/integration-map/index.ts @@ -1,3 +1,3 @@ export * from './integration-map.module'; export * from './integration-map.service'; -export { IntegrationMapSyncEntityCommand } from './commands'; +export * from './commands'; diff --git a/packages/core/src/integration-setting/commands/handlers/integration-setting.get.handler.ts b/packages/core/src/integration-setting/commands/handlers/integration-setting.get.handler.ts index f4512cbabd0..b6fc87457ab 100644 --- a/packages/core/src/integration-setting/commands/handlers/integration-setting.get.handler.ts +++ b/packages/core/src/integration-setting/commands/handlers/integration-setting.get.handler.ts @@ -1,15 +1,12 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { IIntegrationSetting } from '@gauzy/contracts'; -import { RequestContext } from 'core/context'; +import { RequestContext } from '../../../core/context'; import { IntegrationSettingGetCommand } from './../integration-setting.get.command'; import { IntegrationSettingService } from '../../integration-setting.service'; @CommandHandler(IntegrationSettingGetCommand) export class IntegrationSettingGetHandler implements ICommandHandler { - - constructor( - private readonly integrationSettingService: IntegrationSettingService - ) { } + constructor(private readonly integrationSettingService: IntegrationSettingService) {} /** * Executes the 'IntegrationSettingGetCommand' to retrieve an integration setting. diff --git a/packages/core/src/integration-setting/integration-setting.subscriber.ts b/packages/core/src/integration-setting/integration-setting.subscriber.ts index 75f72937b77..87a2cf318e6 100644 --- a/packages/core/src/integration-setting/integration-setting.subscriber.ts +++ b/packages/core/src/integration-setting/integration-setting.subscriber.ts @@ -5,42 +5,39 @@ import { keysToWrapSecrets, sensitiveSecretKeys } from './integration-setting.ut @EventSubscriber() export class IntegrationSettingSubscriber extends BaseEntityEventSubscriber { - /** - * Indicates that this subscriber only listen to IntegrationSetting events. - */ - listenTo() { - return IntegrationSetting; - } + /** + * Indicates that this subscriber only listen to IntegrationSetting events. + */ + listenTo() { + return IntegrationSetting; + } - /** - * Called after an IntegrationSetting entity is loaded from the database. This method handles - * sensitive information by partially masking it before presenting to the user. - * - * @param entity The IntegrationSetting entity that has been loaded. - * @returns {Promise} A promise that resolves when the post-load processing is complete. - */ - async afterEntityLoad(entity: IntegrationSetting): Promise { - try { - // Extract sensitive information from the entity - const { settingsName, settingsValue } = entity; + /** + * Called after an IntegrationSetting entity is loaded from the database. This method handles + * sensitive information by partially masking it before presenting to the user. + * + * @param entity The IntegrationSetting entity that has been loaded. + * @returns {Promise} A promise that resolves when the post-load processing is complete. + */ + async afterEntityLoad(entity: IntegrationSetting): Promise { + try { + // Extract sensitive information from the entity + const { settingsName, settingsValue } = entity; + // Specify the percentage of the string to be replaced with the character + const percentage = 25; - // Specify the percentage of the string to be replaced with the character - const percentage = 25; + entity.wrapSecretKey = settingsName; + entity.wrapSecretValue = settingsValue; - if (sensitiveSecretKeys.includes(settingsName) && typeof settingsValue === 'string') { - // Create an object containing the sensitive data - const secrets: Record = { - [settingsName]: settingsValue, - }; - - // Apply the wrapping function only to the sensitive keys - const wrapped = keysToWrapSecrets(sensitiveSecretKeys, secrets, percentage); - - entity.wrapSecretKey = settingsName; - entity.wrapSecretValue = wrapped[settingsName]; - } - } catch (error) { - console.error('IntegrationSettingSubscriber: An error occurred during the afterEntityLoad process:', error); - } - } + if (sensitiveSecretKeys.includes(settingsName) && typeof settingsValue === 'string') { + // Create an object containing the sensitive data + const secrets: Record = { [settingsName]: settingsValue }; + // Apply the wrapping function only to the sensitive keys + const wrapped = keysToWrapSecrets(sensitiveSecretKeys, secrets, percentage); + entity.wrapSecretValue = wrapped[settingsName]; + } + } catch (error) { + console.error('IntegrationSettingSubscriber: An error occurred during the afterEntityLoad process:', error); + } + } } diff --git a/packages/core/src/integration-tenant/commands/handlers/integration-tenant-update-or-create.handler.ts b/packages/core/src/integration-tenant/commands/handlers/integration-tenant-update-or-create.handler.ts index 8c0d7150a9b..4831cb07320 100644 --- a/packages/core/src/integration-tenant/commands/handlers/integration-tenant-update-or-create.handler.ts +++ b/packages/core/src/integration-tenant/commands/handlers/integration-tenant-update-or-create.handler.ts @@ -9,11 +9,10 @@ import { IntegrationTenantUpdateCommand } from '../integration-tenant.update.com @Injectable() @CommandHandler(IntegrationTenantUpdateOrCreateCommand) export class IntegrationTenantUpdateOrCreateHandler implements ICommandHandler { - constructor( private readonly _commandBus: CommandBus, private readonly _integrationTenantService: IntegrationTenantService - ) { } + ) {} /** * Execute the IntegrationTenantUpdateOrCreateCommand to update or create an integration tenant. @@ -29,14 +28,10 @@ export class IntegrationTenantUpdateOrCreateHandler implements ICommandHandler { + constructor(private readonly _integrationTenantService: IntegrationTenantService) {} - constructor( - private readonly _integrationTenantService: IntegrationTenantService - ) { } - - public async execute( - command: IntegrationTenantCreateCommand - ): Promise { + public async execute(command: IntegrationTenantCreateCommand): Promise { try { const { input } = command; return await this._integrationTenantService.create(input); diff --git a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.delete.handler.ts b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.delete.handler.ts index 1ff9bcaf9f3..881495ca9a9 100644 --- a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.delete.handler.ts +++ b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.delete.handler.ts @@ -1,66 +1,59 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { DeleteResult } from 'typeorm'; import { IntegrationEnum } from '@gauzy/contracts'; -import { GithubInstallationDeleteCommand } from './../../../integration/github/commands'; +import { RequestContext } from '../../../core/context'; +import { EventBus } from '../../../event-bus/event-bus'; +import { BaseEntityEventTypeEnum } from '../../../event-bus/base-entity-event'; +import { IntegrationEvent } from '../../../event-bus/events'; import { IntegrationTenantService } from '../../integration-tenant.service'; import { IntegrationTenantDeleteCommand } from '../integration-tenant.delete.command'; -import { DeleteResult } from 'typeorm'; @CommandHandler(IntegrationTenantDeleteCommand) export class IntegrationTenantDeleteHandler implements ICommandHandler { + constructor( + private readonly _integrationTenantService: IntegrationTenantService, + private readonly _eventBus: EventBus + ) {} - constructor( - private readonly _commandBus: CommandBus, - private readonly _integrationTenantService: IntegrationTenantService - ) { } - - /** - * Execute the command to delete the integration tenant. - * @param command - The IntegrationTenantDeleteCommand instance. - */ - public async execute(command: IntegrationTenantDeleteCommand): Promise { - try { - // Extract information from the command - const { id, options } = command; - const { tenantId, organizationId } = options; + /** + * Execute the command to delete the integration tenant. + * @param command - The IntegrationTenantDeleteCommand instance. + */ + public async execute(command: IntegrationTenantDeleteCommand): Promise { + try { + // Extract information from the command + const { id, options } = command; + const { tenantId, organizationId } = options; - // Find the integration tenant by ID along with related data - const integration = await this._integrationTenantService.findOneByIdString(id, { - where: { - tenantId, - organizationId, - }, - relations: { - integration: true, - settings: true - } - }); + // Find the integration tenant by ID along with related data + const integration = await this._integrationTenantService.findOneByIdString(id, { + where: { tenantId, organizationId }, + relations: { integration: true, settings: true } + }); - // Check the provider type of the integration and perform actions accordingly - switch (integration.integration.provider) { - case IntegrationEnum.GITHUB: - // Execute a command to delete GitHub installation - this._commandBus.execute( - new GithubInstallationDeleteCommand(integration) - ); - break; - // Add cases for other integration providers if needed - default: - // Handle other integration providers if needed - break; - } + // Check the provider type of the integration and perform actions accordingly + switch (integration.integration.provider) { + case IntegrationEnum.GITHUB: + // Publish the integration delete event + const ctx = RequestContext.currentRequestContext(); + const event = new IntegrationEvent(ctx, integration, BaseEntityEventTypeEnum.DELETED); + await this._eventBus.publish(event); + break; + // Add cases for other integration providers if needed + default: + // Handle other integration providers if needed + break; + } - // Delete the integration tenant - return await this._integrationTenantService.delete(id, { - where: { - tenantId, - organizationId, - } - }); - } catch (error) { - // Handle errors and return an appropriate error response - console.log(`Failed to delete integration tenant: %s`, error.message); - throw new HttpException(`Failed to delete integration tenant: ${error.message}`, HttpStatus.BAD_REQUEST); - } - } + // Delete the integration tenant + return await this._integrationTenantService.delete(id, { + where: { tenantId, organizationId } + }); + } catch (error) { + // Handle errors and return an appropriate error response + console.log(`Failed to delete integration tenant: %s`, error.message); + throw new HttpException(`Failed to delete integration tenant: ${error.message}`, HttpStatus.BAD_REQUEST); + } + } } diff --git a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.get.handler.ts b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.get.handler.ts index 87324726cc9..e34915ec94e 100644 --- a/packages/core/src/integration-tenant/commands/handlers/integration-tenant.get.handler.ts +++ b/packages/core/src/integration-tenant/commands/handlers/integration-tenant.get.handler.ts @@ -6,20 +6,18 @@ import { IntegrationTenant } from '../../../integration-tenant/integration-tenan @CommandHandler(IntegrationTenantGetCommand) export class IntegrationTenantGetHandler implements ICommandHandler { + constructor(private readonly _integrationTenantService: IntegrationTenantService) {} - constructor( - private readonly _integrationTenantService: IntegrationTenantService - ) { } - - public async execute( - command: IntegrationTenantGetCommand - ): Promise { + public async execute(command: IntegrationTenantGetCommand): Promise { 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); + 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 a1be2a7eefd..6fe8eca2405 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 @@ -2,87 +2,79 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { CommandBus, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { IIntegrationSetting, IIntegrationTenant, IIntegrationTenantUpdateInput } from '@gauzy/contracts'; import { isNotEmpty } from '@gauzy/common'; -import { RequestContext } from 'core/context'; +import { RequestContext } from '../../../core/context'; import { IntegrationTenantUpdateCommand } from '../../commands'; import { IntegrationTenantService } from '../../integration-tenant.service'; import { IntegrationTenant } from '../../integration-tenant.entity'; -import { IntegrationSettingUpdateCommand } from 'integration-setting/commands'; +import { IntegrationSettingUpdateCommand } from '../../../integration-setting/commands'; @CommandHandler(IntegrationTenantUpdateCommand) export class IntegrationTenantUpdateHandler implements ICommandHandler { + constructor( + private readonly _commandBus: CommandBus, + private readonly _integrationTenantService: IntegrationTenantService + ) {} - constructor( - private readonly _commandBus: CommandBus, - private readonly _integrationTenantService: IntegrationTenantService - ) { } + public async execute(command: IntegrationTenantUpdateCommand): Promise { + try { + const { id, input } = command; + return await this.update(id, input); + } catch (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); + } + } - public async execute( - command: IntegrationTenantUpdateCommand - ): Promise { - try { - const { id, input } = command; - return await this.update(id, input); - } catch (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); - } - } + /** + * Update an integration tenant with the provided data. + * @param id The ID of the integration tenant to update. + * @param request The data to update the integration tenant. + * @returns A promise that resolves to the updated integration tenant. + */ + public async update( + integrationId: IIntegrationTenant['id'], + request: IIntegrationTenantUpdateInput + ): Promise { + try { + // Determine the current tenant ID from the request context or use the one from the request. + const tenantId = RequestContext.currentTenantId() || request.tenantId; - /** - * Update an integration tenant with the provided data. - * @param id The ID of the integration tenant to update. - * @param request The data to update the integration tenant. - * @returns A promise that resolves to the updated integration tenant. - */ - public async update( - integrationId: IIntegrationTenant['id'], - request: IIntegrationTenantUpdateInput - ): Promise { - try { - // Determine the current tenant ID from the request context or use the one from the request. - const tenantId = RequestContext.currentTenantId() || request.tenantId; + // Extract properties from the request. + let { organizationId, isActive, isArchived, settings = [] } = request; - // Extract properties from the request. - let { organizationId, isActive, isArchived, settings = [] } = request; + // Map and assign 'settings' and 'entitySettings' with tenant and organization IDs + settings = settings.map((item: IIntegrationSetting) => ({ + ...item, + integrationId, + tenantId, + organizationId + })); - // Map and assign 'settings' and 'entitySettings' with tenant and organization IDs - settings = settings.map((item: IIntegrationSetting) => ({ - ...item, - integrationId, - tenantId, - organizationId - })); + // If there are settings to update, execute an update command for integration settings. + if (isNotEmpty(settings)) { + /** + * Executes an update command for integration settings. + * + * @param integrationId - The identifier of the integration to update settings for. + * @param settings - The new settings data to be applied to the integration. + * @returns {Promise} - A promise that resolves when the update command is executed. + */ + await this._commandBus.execute(new IntegrationSettingUpdateCommand(integrationId, settings)); + } - // If there are settings to update, execute an update command for integration settings. - if (isNotEmpty(settings)) { - /** - * Executes an update command for integration settings. - * - * @param integrationId - The identifier of the integration to update settings for. - * @param settings - The new settings data to be applied to the integration. - * @returns {Promise} - A promise that resolves when the update command is executed. - */ - await this._commandBus.execute( - new IntegrationSettingUpdateCommand( - integrationId, - settings - ) - ); - } + // Update the integration tenant's status and archive status. + await this._integrationTenantService.update(integrationId, { + isActive, + isArchived + }); - // Update the integration tenant's status and archive status. - await this._integrationTenantService.update(integrationId, { - isActive, - isArchived - }); - - // Retrieve and return the updated integration tenant. - return await this._integrationTenantService.findOneByIdString(integrationId); - } catch (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); - } - } + // Retrieve and return the updated integration tenant. + return await this._integrationTenantService.findOneByIdString(integrationId); + } catch (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/integration-tenant-update-or-create.command.ts b/packages/core/src/integration-tenant/commands/integration-tenant-update-or-create.command.ts index 2a15218e692..29dc086ffda 100644 --- a/packages/core/src/integration-tenant/commands/integration-tenant-update-or-create.command.ts +++ b/packages/core/src/integration-tenant/commands/integration-tenant-update-or-create.command.ts @@ -1,6 +1,6 @@ import { ICommand } from '@nestjs/cqrs'; import { FindOptionsWhere } from 'typeorm'; -import { IIntegrationTenant, } from '@gauzy/contracts'; +import { IIntegrationTenant } from '@gauzy/contracts'; import { IntegrationTenant } from '../integration-tenant.entity'; export class IntegrationTenantUpdateOrCreateCommand implements ICommand { @@ -9,5 +9,5 @@ export class IntegrationTenantUpdateOrCreateCommand implements ICommand { constructor( public readonly options: FindOptionsWhere, public readonly input: IIntegrationTenant - ) { } + ) {} } diff --git a/packages/core/src/integration-tenant/commands/integration-tenant.create.command.ts b/packages/core/src/integration-tenant/commands/integration-tenant.create.command.ts index 1ff4a93eef6..459641778f4 100644 --- a/packages/core/src/integration-tenant/commands/integration-tenant.create.command.ts +++ b/packages/core/src/integration-tenant/commands/integration-tenant.create.command.ts @@ -4,7 +4,5 @@ import { IIntegrationTenantCreateInput } from '@gauzy/contracts'; export class IntegrationTenantCreateCommand implements ICommand { static readonly type = '[Integration] Create Integration'; - constructor( - public readonly input: IIntegrationTenantCreateInput - ) { } + constructor(public readonly input: IIntegrationTenantCreateInput) {} } diff --git a/packages/core/src/integration-tenant/commands/integration-tenant.delete.command.ts b/packages/core/src/integration-tenant/commands/integration-tenant.delete.command.ts index 13911c96cad..6a7ffcfeebb 100644 --- a/packages/core/src/integration-tenant/commands/integration-tenant.delete.command.ts +++ b/packages/core/src/integration-tenant/commands/integration-tenant.delete.command.ts @@ -1,11 +1,8 @@ import { ICommand } from '@nestjs/cqrs'; -import { IIntegrationTenant, IIntegrationTenantFindInput } from '@gauzy/contracts'; +import { ID, IIntegrationTenantFindInput } from '@gauzy/contracts'; export class IntegrationTenantDeleteCommand implements ICommand { - static readonly type = '[Integration] Delete Integration'; + static readonly type = '[Integration] Delete Integration'; - constructor( - public readonly id: IIntegrationTenant['id'], - public readonly options: IIntegrationTenantFindInput - ) { } + constructor(public readonly id: ID, public readonly options: IIntegrationTenantFindInput) {} } diff --git a/packages/core/src/integration-tenant/commands/integration-tenant.get.command.ts b/packages/core/src/integration-tenant/commands/integration-tenant.get.command.ts index bbc4fe54aa0..c7c03eb4790 100644 --- a/packages/core/src/integration-tenant/commands/integration-tenant.get.command.ts +++ b/packages/core/src/integration-tenant/commands/integration-tenant.get.command.ts @@ -5,7 +5,5 @@ import { IntegrationTenant } from './../integration-tenant.entity'; export class IntegrationTenantGetCommand implements ICommand { static readonly type = '[Integration] Get Integration'; - constructor( - public readonly input: FindOneOptions - ) { } + constructor(public readonly input: FindOneOptions) {} } diff --git a/packages/core/src/integration-tenant/commands/integration-tenant.update.command.ts b/packages/core/src/integration-tenant/commands/integration-tenant.update.command.ts index 209133ef587..bf8aed87f94 100644 --- a/packages/core/src/integration-tenant/commands/integration-tenant.update.command.ts +++ b/packages/core/src/integration-tenant/commands/integration-tenant.update.command.ts @@ -1,11 +1,8 @@ import { ICommand } from '@nestjs/cqrs'; -import { IIntegrationTenant, IIntegrationTenantUpdateInput } from '@gauzy/contracts'; +import { ID, IIntegrationTenantUpdateInput } from '@gauzy/contracts'; export class IntegrationTenantUpdateCommand implements ICommand { - static readonly type = '[Integration] Update Integration'; + static readonly type = '[Integration] Update Integration'; - constructor( - public readonly id: IIntegrationTenant['id'], - public readonly input: IIntegrationTenantUpdateInput - ) { } + constructor(public readonly id: ID, public readonly input: IIntegrationTenantUpdateInput) {} } diff --git a/packages/core/src/integration-tenant/dto/update-integration-tenant.dto.ts b/packages/core/src/integration-tenant/dto/update-integration-tenant.dto.ts index 4501bc1dec8..e1e47948245 100644 --- a/packages/core/src/integration-tenant/dto/update-integration-tenant.dto.ts +++ b/packages/core/src/integration-tenant/dto/update-integration-tenant.dto.ts @@ -1,12 +1,14 @@ -import { IIntegrationTenantUpdateInput } from "@gauzy/contracts"; -import { IntersectionType, PickType } from "@nestjs/swagger"; -import { TenantOrganizationBaseDTO } from "core/dto"; +import { IIntegrationTenantUpdateInput } from '@gauzy/contracts'; +import { IntersectionType, PickType } from '@nestjs/swagger'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; import { IntegrationTenant } from '../integration-tenant.entity'; /** * Represent a DTO (Data Transfer Object) for updating an integration tenant. */ -export class UpdateIntegrationTenantDTO extends IntersectionType( - TenantOrganizationBaseDTO, // Extends properties from the 'TenantOrganizationBaseDTO' type. - PickType(IntegrationTenant, ['isActive', 'isArchived']) // Extends specific properties from the 'IntegrationTenant' type. -) implements Partial { } +export class UpdateIntegrationTenantDTO + extends IntersectionType( + TenantOrganizationBaseDTO, // Extends properties from the 'TenantOrganizationBaseDTO' type. + PickType(IntegrationTenant, ['isActive', 'isArchived']) // Extends specific properties from the 'IntegrationTenant' type. + ) + implements Partial {} diff --git a/packages/core/src/integration-tenant/integration-tenant.controller.ts b/packages/core/src/integration-tenant/integration-tenant.controller.ts index 8f03e2f3c4f..b208a6b61d2 100644 --- a/packages/core/src/integration-tenant/integration-tenant.controller.ts +++ b/packages/core/src/integration-tenant/integration-tenant.controller.ts @@ -12,12 +12,11 @@ import { HttpException } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; -import { ApiTags } from '@nestjs/swagger'; -import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { DeleteResult } from 'typeorm'; -import { IIntegrationTenant, IPagination, PermissionsEnum } from '@gauzy/contracts'; -import { CrudController, PaginationParams } from 'core/crud'; -import { TenantOrganizationBaseDTO } from 'core/dto'; +import { ID, IIntegrationTenant, IPagination, PermissionsEnum } from '@gauzy/contracts'; +import { CrudController, PaginationParams } from '../core/crud'; +import { TenantOrganizationBaseDTO } from '../core/dto'; import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; import { Permissions } from './../shared/decorators'; import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; @@ -84,7 +83,7 @@ export class IntegrationTenantController extends CrudController { try { @@ -97,7 +96,7 @@ export class IntegrationTenantController extends CrudController { - try { - // Update the corresponding integration tenant with the new input data - return await this._commandBus.execute(new IntegrationTenantUpdateCommand(id, input)); - } catch (error) { - // Handle errors, e.g., return an error response. - throw new Error('Failed to update integration fields'); - } + // Update the corresponding integration tenant with the new input data + return await this._commandBus.execute(new IntegrationTenantUpdateCommand(id, input)); } /** @@ -132,13 +126,16 @@ export class IntegrationTenantController extends CrudController { 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); + throw new HttpException( + 'Missing or invalid organizationId in the query parameters', + HttpStatus.BAD_REQUEST + ); } // Execute a command to delete the resource using a command bus diff --git a/packages/core/src/integration-tenant/integration-tenant.module.ts b/packages/core/src/integration-tenant/integration-tenant.module.ts index 8ed8ceef953..62cae6ea04a 100644 --- a/packages/core/src/integration-tenant/integration-tenant.module.ts +++ b/packages/core/src/integration-tenant/integration-tenant.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { RouterModule } from '@nestjs/core'; import { CqrsModule } from '@nestjs/cqrs'; import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { EventBusModule } from '../event-bus/event-bus.module'; import { IntegrationTenantController } from './integration-tenant.controller'; import { IntegrationTenantService } from './integration-tenant.service'; import { IntegrationTenant } from './integration-tenant.entity'; @@ -15,19 +16,18 @@ import { TypeOrmIntegrationTenantRepository } from './repository'; @Module({ imports: [ - RouterModule.register([ - { path: '/integration-tenant', module: IntegrationTenantModule } - ]), + RouterModule.register([{ path: '/integration-tenant', module: IntegrationTenantModule }]), TypeOrmModule.forFeature([IntegrationTenant]), MikroOrmModule.forFeature([IntegrationTenant]), RoleModule, RolePermissionModule, forwardRef(() => IntegrationSettingModule), forwardRef(() => IntegrationEntitySettingModule), - CqrsModule + CqrsModule, + EventBusModule ], controllers: [IntegrationTenantController], providers: [IntegrationTenantService, TypeOrmIntegrationTenantRepository, ...CommandHandlers], - exports: [TypeOrmModule, MikroOrmModule, IntegrationTenantService, TypeOrmIntegrationTenantRepository], + exports: [TypeOrmModule, MikroOrmModule, IntegrationTenantService, TypeOrmIntegrationTenantRepository] }) -export class IntegrationTenantModule { } +export class IntegrationTenantModule {} diff --git a/packages/core/src/integration/gauzy-ai/integration-ai.middleware.ts b/packages/core/src/integration/gauzy-ai/integration-ai.middleware.ts index 62ec41445b2..aa459f94f92 100644 --- a/packages/core/src/integration/gauzy-ai/integration-ai.middleware.ts +++ b/packages/core/src/integration/gauzy-ai/integration-ai.middleware.ts @@ -1,12 +1,12 @@ import { Inject, Injectable, NestMiddleware } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Cache } from 'cache-manager'; import { Request, Response, NextFunction } from 'express'; import { isNotEmpty } from '@gauzy/common'; import { IIntegrationSetting, IntegrationEnum } from '@gauzy/contracts'; import { RequestConfigProvider } from '@gauzy/integration-ai'; import { arrayToObject } from './../../core/utils'; import { IntegrationTenantService } from './../../integration-tenant/integration-tenant.service'; -import { Cache } from 'cache-manager'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; @Injectable() export class IntegrationAIMiddleware implements NestMiddleware { @@ -16,8 +16,14 @@ export class IntegrationAIMiddleware implements NestMiddleware { @Inject(CACHE_MANAGER) private cacheManager: Cache, private readonly _integrationTenantService: IntegrationTenantService, private readonly _requestConfigProvider: RequestConfigProvider - ) { } - + ) {} + + /** + * + * @param request + * @param _response + * @param next + */ async use(request: Request, _response: Response, next: NextFunction) { try { // Extract tenant and organization IDs from request headers and body @@ -41,7 +47,9 @@ export class IntegrationAIMiddleware implements NestMiddleware { // Check if tenant and organization IDs are not empty if (isNotEmpty(tenantId) && isNotEmpty(organizationId)) { - console.log(`Getting Gauzy AI integration settings from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}`); + console.log( + `Getting Gauzy AI integration settings from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}` + ); const cacheKey = `integrationTenantSettings_${tenantId}_${organizationId}_${IntegrationEnum.GAUZY_AI}`; @@ -49,7 +57,9 @@ export class IntegrationAIMiddleware implements NestMiddleware { let integrationTenantSettings: IIntegrationSetting[] = await this.cacheManager.get(cacheKey); if (!integrationTenantSettings) { - console.log(`Gauzy AI integration settings NOT loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}`); + console.log( + `Gauzy AI integration settings NOT loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}` + ); const fromDb = await this._integrationTenantService.getIntegrationTenantSettings({ tenantId, @@ -60,13 +70,17 @@ export class IntegrationAIMiddleware implements NestMiddleware { if (fromDb && fromDb.settings) { integrationTenantSettings = fromDb.settings; - const ttl = 5 * 60 * 1000 // 5 min caching period for Integration Tenant Settings + const ttl = 5 * 60 * 1000; // 5 min caching period for Integration Tenant Settings await this.cacheManager.set(cacheKey, integrationTenantSettings, ttl); - console.log(`Gauzy AI integration settings loaded from DB and stored in Cache for tenantId: ${tenantId}, organizationId: ${organizationId}`); + console.log( + `Gauzy AI integration settings loaded from DB and stored in Cache for tenantId: ${tenantId}, organizationId: ${organizationId}` + ); } } else { - console.log(`Gauzy AI integration settings loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}`); + console.log( + `Gauzy AI integration settings loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}` + ); } if (integrationTenantSettings && integrationTenantSettings.length > 0) { 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 ed5b299038d..c5ca609c2db 100644 --- a/packages/core/src/integration/gauzy-ai/integration-ai.module.ts +++ b/packages/core/src/integration/gauzy-ai/integration-ai.module.ts @@ -51,11 +51,11 @@ export class IntegrationAIModule implements NestModule { method: RequestMethod.POST }, { - path: '/employee/job-statistics', + path: '/employee-job/statistics', method: RequestMethod.GET }, { - path: '/employee/:id/job-search-status', + path: '/employee-job/:id/job-search-status', method: RequestMethod.PUT }, { diff --git a/packages/core/src/integration/github/commands/github-installation.delete.command.ts b/packages/core/src/integration/github/commands/github-installation.delete.command.ts deleted file mode 100644 index 6f77c424d34..00000000000 --- a/packages/core/src/integration/github/commands/github-installation.delete.command.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IIntegrationTenant } from "@gauzy/contracts"; - -export class GithubInstallationDeleteCommand { - - constructor( - public readonly integration: IIntegrationTenant - ) { } -} diff --git a/packages/core/src/integration/github/commands/task.update-or-create.command.ts b/packages/core/src/integration/github/commands/task.update-or-create.command.ts deleted file mode 100644 index d3b562b2d2c..00000000000 --- a/packages/core/src/integration/github/commands/task.update-or-create.command.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IGetTaskOptions, ITask } from "@gauzy/contracts"; - -export class GithubTaskUpdateOrCreateCommand { - - constructor( - public readonly task: ITask, - public readonly options: IGetTaskOptions, - ) { } -} 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 deleted file mode 100644 index a0f9dc1d94d..00000000000 --- a/packages/core/src/integration/github/dto/github-app-install.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IGithubAppInstallInput } from "@gauzy/contracts"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsEnum, IsNotEmpty, IsString } from "class-validator"; -import { TenantOrganizationBaseDTO } from "core/dto"; - -export enum GithubSetupActionEnum { - INSTALL = 'install', - UPDATE = 'update' -} - -export class GithubOAuthDTO extends TenantOrganizationBaseDTO implements IGithubAppInstallInput { - @ApiProperty({ type: () => String }) - @IsNotEmpty() - @IsString() - readonly code: string; -} - -export class GithubAppInstallDTO implements IGithubAppInstallInput { - - @ApiPropertyOptional({ type: () => String }) - @IsNotEmpty() - @IsString() - readonly installation_id: string; - - @ApiPropertyOptional({ type: () => String }) - @IsNotEmpty() - @IsEnum(GithubSetupActionEnum) - readonly setup_action: GithubSetupActionEnum; -} diff --git a/packages/core/src/integration/github/dto/github-issues-query.dto.ts b/packages/core/src/integration/github/dto/github-issues-query.dto.ts deleted file mode 100644 index 207deb44dcd..00000000000 --- a/packages/core/src/integration/github/dto/github-issues-query.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, Max, Min } from 'class-validator'; -import { Transform, TransformFnParams } from 'class-transformer'; -import { IGithubIssueFindInput } from '@gauzy/contracts'; -import { TenantOrganizationBaseDTO } from '../../../core/dto'; - -export class GithubIssuesQueryDTO extends TenantOrganizationBaseDTO implements IGithubIssueFindInput { - /** - * Limit (paginated) - max number of entities should be taken. - */ - @ApiPropertyOptional({ type: () => 'number', minimum: 0, maximum: 100 }) - @IsOptional() - @Min(0) - @Max(100) - @Transform((params: TransformFnParams) => parseInt(params.value, 10)) - readonly per_page: number; - - /** - * Offset (paginated) where from entities should be taken. - */ - @ApiPropertyOptional({ type: () => 'number', minimum: 0 }) - @IsOptional() - @Min(0) - @Transform((params: TransformFnParams) => parseInt(params.value, 10)) - readonly page: number; -} diff --git a/packages/core/src/integration/github/dto/process-github-issue-sync.dto.ts b/packages/core/src/integration/github/dto/process-github-issue-sync.dto.ts deleted file mode 100644 index f770d174a1c..00000000000 --- a/packages/core/src/integration/github/dto/process-github-issue-sync.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IGithubIssue, IGithubSyncIssuePayload, IOrganizationGithubRepository, IOrganizationProject } from "@gauzy/contracts"; -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsArray, IsObject, IsOptional, IsUUID } from "class-validator"; -import { TenantOrganizationBaseDTO } from "core/dto"; - -/** - * Data Transfer Object for processing GitHub issue synchronization. - * - * This DTO provides optional properties to handle GitHub issues and repositories during synchronization. - */ -export class ProcessGithubIssueSyncDTO extends TenantOrganizationBaseDTO implements IGithubSyncIssuePayload { - /** Optional array of GitHub issues to synchronize. */ - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsArray() - readonly issues: IGithubIssue[]; - - /** Optional GitHub repository for synchronization. */ - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsObject() - readonly repository: IOrganizationGithubRepository; - - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsUUID() - readonly projectId: IOrganizationProject['id']; -} diff --git a/packages/core/src/integration/github/github-authorization.controller.ts b/packages/core/src/integration/github/github-authorization.controller.ts deleted file mode 100644 index a523f7affec..00000000000 --- a/packages/core/src/integration/github/github-authorization.controller.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 { IGithubIntegrationConfig, 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 github callback query data', HttpStatus.BAD_REQUEST); - } - - /** Github Config Options */ - const { postInstallUrl } = this._config.get('github') as IGithubIntegrationConfig; - - /** 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); - - /** Redirect to the URL */ - if (query.state.startsWith('http')) { - return response.redirect(`${query.state}?${urlParams.toString()}`); - } else { - return response.redirect(`${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-entity-settings.ts b/packages/core/src/integration/github/github-entity-settings.ts deleted file mode 100644 index 5567cfa8e3c..00000000000 --- a/packages/core/src/integration/github/github-entity-settings.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IntegrationEntity } from '@gauzy/contracts'; - -export const DEFAULT_ENTITY_SETTINGS = [ - { - entity: IntegrationEntity.ISSUE, - sync: true - } -]; - -export const ISSUE_TIED_ENTITIES = [ - { - entity: IntegrationEntity.LABEL, - sync: true - } -]; diff --git a/packages/core/src/integration/github/github-sync.service.ts b/packages/core/src/integration/github/github-sync.service.ts deleted file mode 100644 index 5ed30ad0307..00000000000 --- a/packages/core/src/integration/github/github-sync.service.ts +++ /dev/null @@ -1,711 +0,0 @@ - -import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { CommandBus } from '@nestjs/cqrs'; -import * as moment from 'moment'; -import * as chalk from 'chalk'; -import { Request } from 'express'; -import { OctokitService } from '@gauzy/integration-github'; -import { - IGithubAutomationIssuePayload, - IGithubIssue, - IGithubIssueLabel, - IGithubSyncIssuePayload, - IGithubInstallationDeletedPayload, - IIntegrationEntitySetting, - IIntegrationEntitySettingTied, - IIntegrationTenant, - IOrganization, - IOrganizationProject, - ITag, - IntegrationEntity, - TaskStatusEnum, - IGithubIssueCreateOrUpdatePayload, - IOrganizationGithubRepository, - IIntegrationMap, - GithubRepositoryStatusEnum, - SYNC_TAG_GAUZY, - SYNC_TAG_GITHUB -} from '@gauzy/contracts'; -import { isNotEmpty, sleep } from '@gauzy/common'; -import { RequestContext } from 'core/context'; -import { arrayToObject } from 'core/utils'; -import { IntegrationTenantService } from 'integration-tenant/integration-tenant.service'; -import { OrganizationProjectService } from 'organization-project/organization-project.service'; -import { IntegrationMapSyncIssueCommand, IntegrationMapSyncLabelCommand } from 'integration-map/commands'; -import { AutomationTaskSyncCommand } from 'tasks/commands'; -import { AutomationLabelSyncCommand } from 'tags/commands'; -import { GithubRepositoryService } from './repository/github-repository.service'; -import { IntegrationSyncGithubRepositoryIssueCommand } from './repository/issue/commands'; - -@Injectable() -export class GithubSyncService { - private readonly logger = new Logger('GithubSyncService'); - - constructor( - private readonly _commandBus: CommandBus, - private readonly _octokitService: OctokitService, - private readonly _integrationTenantService: IntegrationTenantService, - private readonly _organizationProjectService: OrganizationProjectService, - private readonly _githubRepositoryService: GithubRepositoryService, - ) { } - - /** - * Automatically synchronize GitHub issues with a repository. - * - * @param {IIntegrationTenant['id']} integrationId - The ID of the integration tenant. - * @param {IGithubSyncIssuePayload} input - The payload containing GitHub repository details and issues. - * @param {Request} request - The HTTP request object. - * @returns {Promise} A Promise that indicates whether the synchronization was successful. - */ - public async autoSyncGithubIssues( - integrationId: IIntegrationTenant['id'], - input: IGithubSyncIssuePayload, - request: Request - ): Promise { - // 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); - } - - try { - // Extract the 'repository' object from the input payload - const repository: IOrganizationGithubRepository = input.repository; - - try { - // Extract the 'installation_id' from the integration settings - const installation_id = settings['installation_id']; - - // Extract repository details - const { name: repo, owner } = repository; - - // Retrieve GitHub issues for the repository - this.getRepositoryAllIssues(installation_id, owner, repo, (issues: IGithubIssue[]) => { - console.log(chalk.magenta(`Automatically syncing ${issues.length} issues`)); - - // Map the issues to the desired format using '_mapIssuePayload' method - input.issues = this._mapIssuePayload(Array.isArray(issues) ? issues : [issues]); - - // Define a delay of 100 milliseconds - const delay: number = 100; - - // Attempt to synchronize GitHub issues using the 'syncingGithubIssues' method - this.syncingGithubIssues(integrationId, input, delay, async () => { - // Update the status of the GitHub repository to "Success" (GithubRepositoryStatusEnum.SUCCESSFULLY). - await this._githubRepositoryService.update(repository.id, { - status: GithubRepositoryStatusEnum.SUCCESSFULLY - }); - }, async () => { - // Handle the error by updating the status of the GitHub repository to "Error" (GithubRepositoryStatusEnum.ERROR). - await this._githubRepositoryService.update(repository.id, { - status: GithubRepositoryStatusEnum.ERROR - }); - }); - }); - - return true; // Return true to indicate a successful synchronization. - } catch (error) { - console.log('Error while syncing github issues automatically: %s', error.message); - // Handle the error by updating the status of the GitHub repository to "Error" (GithubRepositoryStatusEnum.ERROR). - await this._githubRepositoryService.update(repository.id, { - status: GithubRepositoryStatusEnum.ERROR - }); - - return false; // Return false to indicate that an error occurred during synchronization. - } - } catch (error) { - // Handle errors gracefully, for example, log them - this.logger.error('Error in sync github issues and labels automatically', error.message); - throw new HttpException({ message: 'GitHub automatic synchronization failed', error }, HttpStatus.BAD_REQUEST); - } - } - - /** - * Manually synchronize GitHub issues with a repository. - * - * @param {IIntegrationTenant['id']} integrationId - The ID of the integration tenant. - * @param {IGithubSyncIssuePayload} input - The payload containing GitHub repository details and issues. - * @param {Request} request - The HTTP request object. - * @returns {Promise} A Promise indicating whether the synchronization was successful. - */ - public async manualSyncGithubIssues( - integrationId: IIntegrationTenant['id'], - input: IGithubSyncIssuePayload, - request: Request - ): Promise { - try { - // 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); - } - - // Extract the 'repository' object from the input payload - const repository: IOrganizationGithubRepository = input.repository; - - try { - // Set a delay of 0 milliseconds - const delay: number = 0; - - // Attempt to synchronize GitHub issues using the syncGithubIssues method. - await this.syncingGithubIssues(integrationId, input, delay); - - // Update the status of the GitHub repository to "Success" (GithubRepositoryStatusEnum.SUCCESSFULLY). - await this._githubRepositoryService.update(repository.id, { - status: GithubRepositoryStatusEnum.SUCCESSFULLY - }); - - return true; // Return true to indicate a successful synchronization. - } catch (error) { - // Handle the error by updating the status of the GitHub repository to "Error" (GithubRepositoryStatusEnum.ERROR). - await this._githubRepositoryService.update(repository.id, { - status: GithubRepositoryStatusEnum.ERROR - }); - - return false; // Return false to indicate that an error occurred during synchronization. - } - } catch (error) { - // Handle errors gracefully, for example, log them - this.logger.error('Error in sync GitHub issues and labels manually', error.message); - - // Throw an HTTP exception to indicate manual synchronization failure. - throw new HttpException({ message: 'GitHub manual synchronization failed', error }, HttpStatus.BAD_REQUEST); - } - } - - /** - * Synchronize GitHub issues and labels based on entity settings. - * - * @param integrationId - The ID of the integration tenant. - * @param input - The payload containing information required for synchronization. - * @throws {HttpException} Throws an HTTP exception if synchronization fails. - */ - public async syncingGithubIssues( - integrationId: IIntegrationTenant['id'], - input: IGithubSyncIssuePayload, - delay: number = 100, - successCallback?: (success: boolean) => void, - errorCallback?: (error: boolean) => void, - ): Promise { - try { - const { organizationId, repository } = input; - - const tenantId = RequestContext.currentTenantId() || input.tenantId; - const issues: IGithubIssue[] = Array.isArray(input.issues) ? input.issues : [input.issues]; - - // Step 1: Retrieve integration settings tied to the specified organization - const { entitySettings } = await this._integrationTenantService.findOneByIdString(integrationId, { - where: { - tenantId, - organizationId, - isActive: true, - isArchived: false - }, - relations: { - entitySettings: { - tiedEntities: true - } - } - }); - - try { - // Step 2: Initialize an array for integration mapping - let integrationMaps: IIntegrationMap[] = []; - - // Step 3: Synchronize data based on entity settings - for await (const entitySetting of entitySettings) { - switch (entitySetting.entity) { - case IntegrationEntity.ISSUE: - // Step 4: Issue synchronization - const issueSetting: IIntegrationEntitySetting = entitySetting; - if (!!issueSetting.sync) { - for await (const issue of issues) { - console.log(chalk.green(`Processing Issue Sync: %s`), issue.id); - const { id, title, state, body } = issue; - - let tags: ITag[] = []; - try { - // Step 5: Label synchronization settings - const labelSetting: IIntegrationEntitySetting = entitySetting.tiedEntities.find( - ({ entity }: IIntegrationEntitySettingTied) => entity === IntegrationEntity.LABEL - ); - if (!!labelSetting && labelSetting.sync) { - // Step 6: Sync GitHub Issue Labels - tags = await this.syncGithubLabelsByIssueNumber({ - organizationId, - tenantId, - integrationId, - repository, - issue - }); - } - } catch (error) { - console.error('Failed to fetch GitHub labels for the repository issue:', error.message); - } - - // Step 7: Synchronized GitHub Repository Issue. - const { repositoryId } = repository; - await this._commandBus.execute( - new IntegrationSyncGithubRepositoryIssueCommand( - { - tenantId, - organizationId, - integrationId - }, - repositoryId, - issue - ) - ); - - // Step 8: Execute a command to initiate the synchronization process - const triggeredEvent = false; - const integrationMap = await this._commandBus.execute( - new IntegrationMapSyncIssueCommand({ - entity: { - title, - description: body, - status: state as TaskStatusEnum, - public: repository.private, - projectId: input['projectId'] || null, - tags, - organizationId, - tenantId - }, - sourceId: id.toString(), - integrationId, - organizationId, - tenantId - }, triggeredEvent) - ); - integrationMaps.push(integrationMap); - - /** 100ms Pause or Delay for sync new sync issue */ - await sleep(delay); - } - } - break; - } - } - - // Step 9: Update Integration Last Synced Date - await this._integrationTenantService.update(integrationId, { - lastSyncedAt: moment() - }); - - // Call the success callback function if provided - if (successCallback) { - successCallback(true); - } - - // Step 10: Return integration mapping - return integrationMaps; - } catch (error) { - console.log('Error while syncing github issues: ', error.message); - // Call the error callback function if provided - if (errorCallback) { - errorCallback(false); - } - - return false; - } - } catch (error) { - // Handle errors gracefully, for example, log them - this.logger.error('Error in sync github issues and labels manual', error.message); - throw new HttpException({ message: 'GitHub manual synchronization failed', error }, HttpStatus.BAD_REQUEST); - } - } - - /** - * Synchronize GitHub labels for a specific repository issue based on integration settings. - * - * @param organizationId - The ID of the organization. - * @param tenantId - The ID of the organization's tenant. - * @param integrationId - The ID of the GitHub integration. - * @param repository - Information about the GitHub repository for which labels are synchronized. - * @param issue - The GitHub issue for which labels are synchronized. - * @returns A promise that resolves to the result of the label synchronization process, which is an array of tags. - */ - private async syncGithubLabelsByIssueNumber({ - organizationId, - tenantId, - integrationId, - repository, - issue - }: { - organizationId: IOrganization['id'], - tenantId: IOrganization['tenantId'], - integrationId: IIntegrationTenant['id'], - repository: IOrganizationGithubRepository, - issue: IGithubIssue - }): Promise { - try { - // Retrieve integration settings - const integration = await this._integrationTenantService.findOneByIdString(integrationId, { - where: { - isActive: true, - isArchived: false - }, - relations: { - settings: true - } - }); - - const settings = arrayToObject(integration.settings, 'settingsName', 'settingsValue'); - const { name: repo, owner } = repository; - - // Check for integration settings and installation ID - if (settings && settings.installation_id) { - const installation_id = settings.installation_id; - - // Get the labels associated with the GitHub issue - let labels = issue.labels; - - // List of labels to check and create if missing - const labelsToCheck = [SYNC_TAG_GITHUB, SYNC_TAG_GAUZY]; - const labelsToCreate = labelsToCheck.filter( - (name) => !labels.find((label: IGithubIssueLabel) => label.name === name) - ); - - // Check if specific labels exist on a GitHub issue and create them if missing. - if (isNotEmpty(labelsToCreate)) { - try { - const response = await this._octokitService.addLabelsForIssue(installation_id, { - owner, - repo, - issue_number: issue.number, - labels: labelsToCreate - }); - labels = response.data; - } catch (error) { - console.log('Error while creating missing labels: ', error.message); - } - } - - // Sync labels and return an array of tags - return await Promise.all( - labels.map( - async (label: IGithubIssueLabel) => { - const { id: sourceId, name, color, description } = label; - - return await this._commandBus.execute( - new IntegrationMapSyncLabelCommand({ - entity: { - name, - color, - description, - isSystem: label.default - }, - sourceId: sourceId.toString(), - integrationId, - organizationId, - tenantId - }) - ); - } - ) - ); - } - return []; - } catch (error) { - // Handle errors and return an appropriate error response - this.logger.error('Failed to fetch GitHub labels for the repository issue', error.message); - return []; - } - } - - /** - * Syncs automation issues for a GitHub repository. - * - * @param integration - The GitHub integration settings. - * @param input - The payload containing information for the synchronization. - */ - public async syncAutomationIssue( - input: IGithubAutomationIssuePayload - ) { - const { integration, repository, issue } = input; - const { entitySettings } = integration; - try { - /** Extract necessary data from integration */ - const tenantId = integration['tenantId']; - const organizationId = integration['organizationId']; - const integrationId = integration['id']; - - /** Get a list of projects for the repository */ - const projects: IOrganizationProject[] = await this._organizationProjectService.getProjectsByGithubRepository(repository.id, { - organizationId, - tenantId, - integrationId - }); - - for await (const project of projects) { - // Check if the issue should be synchronized for this project - if (!!this.shouldSyncIssue(project, issue)) { - - const issues: IGithubIssue[] = this._mapIssuePayload(Array.isArray(issue) ? issue : [issue]); - const projectId = project.id; - - // Synchronize data based on entity settings - for await (const entitySetting of entitySettings) { - switch (entitySetting.entity) { - case IntegrationEntity.ISSUE: - /** Issues Sync */ - const issueSetting: IIntegrationEntitySetting = entitySetting; - if (!!issueSetting.sync) { - for await (const issue of issues) { - const { id, title, state, body, labels = [] } = issue; - - // Initialize an array to store tags - let tags: ITag[] = []; - - // Check for label synchronization settings - try { - const labelSetting: IIntegrationEntitySetting = entitySetting.tiedEntities.find( - ({ entity }: IIntegrationEntitySettingTied) => entity === IntegrationEntity.LABEL - ); - if (!!labelSetting && labelSetting.sync) { - /** Sync GitHub Issue Labels */ - tags = await Promise.all( - labels.map( - async (label: IGithubIssueLabel) => { - const { id: labelId, name, color, description } = label; - /** */ - return await this._commandBus.execute( - new AutomationLabelSyncCommand({ - entity: { - name, - color, - description, - isSystem: label.default - }, - sourceId: labelId.toString(), - integrationId, - organizationId, - tenantId - }, IntegrationEntity.LABEL) - ); - } - ) - ); - } - } catch (error) { - console.error('Failed to fetch GitHub labels for the repository issue:', error.message); - } - - - // Step 7: Synchronized GitHub repository issue. - const repositoryId = repository.id; - await this._commandBus.execute( - new IntegrationSyncGithubRepositoryIssueCommand( - { - tenantId, - organizationId, - integrationId - }, - repositoryId, - issue - ) - ); - - try { - // Synchronize the issue as a task - return await this._commandBus.execute( - new AutomationTaskSyncCommand({ - entity: { - title, - description: body, - status: state as TaskStatusEnum, - public: repository.private, - prefix: project.name.substring(0, 3) || null, - projectId, - organizationId, - tenantId, - tags - }, - sourceId: id.toString(), - integrationId, - integration, - organizationId, - tenantId - }, IntegrationEntity.ISSUE) - ); - } catch (error) { - this.logger.error(`Failed to sync automation github task: ${id}`, error); - } - } - } - } - } - } - } - } catch (error) { - this.logger.error(`Failed to fetch repository: ${repository.id} integration with specific project too sync issue: ${issue.id}`, error); - } - } - - /** - * Determines whether an issue should be synchronized based on project settings. - * - * @param project - The project configuration. - * @param issue - The GitHub issue to be synchronized. - * @returns A boolean indicating whether the issue should be synchronized. - */ - private shouldSyncIssue(project: IOrganizationProject, issue: IGithubIssue): boolean { - if (!project || !project.isTasksAutoSync) { - return false; - } - if (project.isTasksAutoSyncOnLabel) { - return !!issue.labels.find((label) => label.name.trim() === project.syncTag.trim()); - } - return true; - } - - /** - * Deletes a GitHub installation and its associated integration. - * - * @param payload - An object containing the installation and its associated integration. - */ - public async installationDeleted(payload: IGithubInstallationDeletedPayload) { - try { - // Extract the integration ID from the provided integration object - const integrationId = payload.integration.id; - // ToDo delete sync repository with specific project - // const repositories = payload.repositories; - - // Delete the integration associated with the installation - await this._integrationTenantService.delete(integrationId); - } catch (error) { - // Handle errors - this.logger.error(`Failed to delete GitHub integration for installation: ${payload.installation?.id}`, error); - } - } - - /** - * Map GitHub issue payload data to the required format. - * - * @param issues - An array of GitHub issues. - * @returns An array of mapped issue payload data. - */ - private _mapIssuePayload(issues: IGithubIssue[]): any[] { - return issues.map(({ id, number, title, state, body, labels }) => ({ - id, - number, - title, - state, - body, - labels - })); - } - - /** - * Create or Update a GitHub issue on a repository using the specified installation ID. - * - * @param installationId - The GitHub installation ID. - * @param data - The data for the GitHub issue, including repo, owner, title, body, and labels. - * @returns A promise that resolves to the response from GitHub. - */ - public async createOrUpdateIssue( - installationId: number, - data: IGithubIssueCreateOrUpdatePayload - ): Promise { - try { - // Check if a valid installation ID is provided - if (!installationId) { - throw new HttpException('Invalid request parameter', HttpStatus.UNAUTHORIZED); - } - - // Prepare the payload for opening or updating the issue - const payload = { - repo: data.repo, - owner: data.owner, - title: data.title, - body: data.body, - labels: data.labels - }; - - // Create or update the installation issue using the octokit service - if (data.issue_number) { - // Issue number is provided, update the existing issue - const issue_number = data.issue_number; - - try { - // Check if the issue exists - await this._octokitService.getIssueByIssueNumber(installationId, { - repo: payload.repo, - owner: payload.owner, - issue_number: issue_number - }); - - // Issue exists, update it - const issue = await this._octokitService.updateIssue(installationId, issue_number, payload); - return issue.data; - } catch (error) { - // Issue doesn't exist, create a new one - const issue = await this._octokitService.openIssue(installationId, payload); - return issue.data; - } - } else { - // Issue number is not provided, create a new issue - const issue = await this._octokitService.openIssue(installationId, payload); - return issue.data; - } - } catch (error) { - // Handle errors and return an appropriate error response - this.logger.error('Error while creating/updating an issue in GitHub', error.message); - throw new HttpException(`Error while creating/updating an issue in GitHub: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - /** - * Retrieves all issues from a GitHub repository using the GitHub API with pagination. - * - * @param installation_id - The installation ID for the GitHub App. - * @param owner - The owner (user or organization) of the GitHub repository. - * @param repo - The name of the GitHub repository. - * @returns A Promise that resolves to an array of GitHub issues. - */ - async getRepositoryAllIssues( - installation_id: number, - owner: string, - repo: string, - callback?: (issues: IGithubIssue[]) => void - ): Promise { - const per_page = 100; // Number of issues per page (GitHub API maximum is 100) - const issues: IGithubIssue[] = []; - let page = 1; - let hasMoreIssues = true; - - // Use a while to simplify pagination - while (hasMoreIssues) { - try { - // Fetch issues for the current page - const response = await this._octokitService.getRepositoryIssues(installation_id, { - owner, - repo, - page, - per_page - }); - if (Array.isArray(response.data) && response.data.length > 0) { - // Append the retrieved issues to the result array - issues.push(...response.data); - // Check if there are more issues on the next page - hasMoreIssues = response.data.length === per_page; - } else { - // No more issues to retrieve - hasMoreIssues = false; - } - // Increment the page number for the next request - page++; - } catch (error) { - console.error('Error fetching issues:', error); - break; // Exit the loop on error - } - } - - // Call the callback function if provided - if (callback) { - callback(issues); - } - - return issues; - } -} diff --git a/packages/core/src/integration/github/github.hooks.controller.ts b/packages/core/src/integration/github/github.hooks.controller.ts deleted file mode 100644 index 702886850a8..00000000000 --- a/packages/core/src/integration/github/github.hooks.controller.ts +++ /dev/null @@ -1,73 +0,0 @@ -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 - ) { } - - /** - * Handles the 'installation.deleted' event. - * - * @param context - The context object containing information about the event. - */ - @Hook(['installation.deleted']) - async installationDeleted(context: Context) { - if (!context.isBot) { - await this._githubHooksService.installationDeleted(context); - } - } - - /** - * Handles the 'issues.opened' event. - * - * @param context - The context object containing information about the event. - */ - @Hook(['issues.opened']) - async issuesOpened(context: Context) { - if (!context.isBot) { - await this._githubHooksService.issuesOpened(context); - } - } - - /** - * Handles the 'issues.edited' event. - * - * @param context - The context object containing information about the event. - */ - @Hook(['issues.edited']) - async issuesEdited(context: Context) { - if (!context.isBot) { - await this._githubHooksService.issuesEdited(context); - } - } - - /** - * Handles the 'issues.labeled' event. - * - * @param context - The context object containing information about the event. - */ - @Hook(['issues.labeled']) - async issuesLabeled(context: Context) { - if (!context.isBot) { - await this._githubHooksService.issuesLabeled(context); - } - } - - /** - * Handles the 'issues.labeled' event. - * - * @param context - The context object containing information about the event. - */ - @Hook(['issues.unlabeled']) - async issuesUnlabeled(context: Context) { - if (!context.isBot) { - await this._githubHooksService.issuesUnlabeled(context); - } - } -} diff --git a/packages/core/src/integration/github/github.hooks.service.ts b/packages/core/src/integration/github/github.hooks.service.ts deleted file mode 100644 index 1f19d8b3bc3..00000000000 --- a/packages/core/src/integration/github/github.hooks.service.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { CommandBus } from '@nestjs/cqrs'; -import { Context } from 'probot'; -import { - GithubPropertyMapEnum, - IGithubInstallation, - IGithubIssue, - IGithubRepository, - IIntegrationSetting -} from '@gauzy/contracts'; -import { IntegrationSettingGetCommand, IntegrationSettingGetManyCommand } from 'integration-setting/commands'; -import { GithubSyncService } from './github-sync.service'; - -@Injectable() -export class GithubHooksService { - - private readonly logger = new Logger('GithubHooksService'); - - constructor( - private readonly _commandBus: CommandBus, - private readonly _githubSyncService: GithubSyncService - ) { } - - /** - * Handles the 'installation.deleted' event by deleting a GitHub installation, - * its associated repositories, and the integration setting. - * - * @param context - The context object containing event information. - */ - async installationDeleted(context: Context) { - // Extract necessary data from the context - const installation = context.payload['installation'] as IGithubInstallation; - const repositories = context.payload['repositories'] as IGithubRepository[]; - - try { - const installation_id = installation.id; - // Retrieve the integration settings associated with the GitHub installation. - const settings = await this._commandBus.execute( - new IntegrationSettingGetManyCommand({ - where: { - settingsName: GithubPropertyMapEnum.INSTALLATION_ID, - settingsValue: installation_id, - isActive: true, - isArchived: false, - integration: { - isActive: true, - isArchived: false, - } - }, - relations: { - integration: { - settings: true, - entitySettings: { - tiedEntities: true - } - } - } - }) - ); - return await Promise.all( - settings.map( - async (setting: IIntegrationSetting) => { - if (!setting || !setting.integration) { - // No integration or setting found; no action needed. - return; - } - - const integration = setting.integration; - - // Delete the GitHub integration associated with the installation and its repositories - await this._githubSyncService.installationDeleted({ - installation, - integration, - repositories - }); - } - ) - ); - } catch (error) { - // Handle errors - this.logger.error(`Failed to delete GitHub integration for installation: ${installation?.id}`, error); - } - } - - /** - * Handles the 'issues.opened' event from GitHub, syncs automation issues and labels. - * - * @param context - The GitHub webhook event context. - */ - async issuesOpened(context: Context) { - try { - // Extract necessary data from the context - const installation = context.payload['installation'] as IGithubInstallation; - const issue = context.payload['issue'] as IGithubIssue; - const repository = context.payload['repository'] as IGithubRepository; - - /** Synchronizes automation issues for a GitHub installation. */ - await this.syncAutomationIssue({ installation, issue, repository }); - } catch (error) { - this.logger.error('Failed to sync in issues and labels', error.message); - } - } - - /** - * Handles the 'issues.edited' event from GitHub, syncs automation issues and labels. - * - * @param context - The GitHub webhook event context. - */ - async issuesEdited(context: Context) { - try { - // Extract necessary data from the context - const installation = context.payload['installation'] as IGithubInstallation; - const issue = context.payload['issue'] as IGithubIssue; - const repository = context.payload['repository'] as IGithubRepository; - - /** Synchronizes automation issues for a GitHub installation. */ - await this.syncAutomationIssue({ installation, issue, repository }); - } catch (error) { - this.logger.error('Failed to sync in issues and labels', error.message); - } - } - - /** - * Handles the 'issuesLabeled' event from GitHub. - * - * @param context - The GitHub webhook event context. - */ - async issuesLabeled(context: Context) { - try { - // Extract necessary data from the context - const installation = context.payload['installation'] as IGithubInstallation; - const issue = context.payload['issue'] as IGithubIssue; - const repository = context.payload['repository'] as IGithubRepository; - - /** Synchronizes automation issues for a GitHub installation. */ - await this.syncAutomationIssue({ installation, issue, repository }); - } catch (error) { - this.logger.error('Failed to sync in issues and labels', error.message); - } - } - - /** - * Handles the 'issuesUnlabeled' event from GitHub. - * - * @param context - The GitHub webhook event context. - */ - async issuesUnlabeled(context: Context) { - try { - // Extract necessary data from the context - const installation = context.payload['installation'] as IGithubInstallation; - const issue = context.payload['issue'] as IGithubIssue; - const repository = context.payload['repository'] as IGithubRepository; - - /** Synchronizes automation issues for a GitHub installation. */ - await this.syncAutomationIssue({ installation, issue, repository }); - } catch (error) { - this.logger.error('Failed to sync in issues and labels', error.message); - } - } - - /** - * Synchronizes automation issues for a GitHub installation. - * - * @param param0 - An object containing installation, issue, and repository information. - */ - private async syncAutomationIssue({ - installation, - issue, - repository - }: { - installation: IGithubInstallation, - issue: IGithubIssue, - repository: IGithubRepository - }): Promise { - try { - const setting: IIntegrationSetting = await this.getInstallationSetting(installation); - if (!!setting && !!setting.integration) { - const integration = setting.integration; - await this._githubSyncService.syncAutomationIssue({ - integration, - issue, - repository - }); - } - } catch (error) { - this.logger.error(`Failed to sync GitHub automation issue: ${installation?.id}`, error.message); - } - } - - /** - * Retrieves integration settings associated with a specific GitHub installation. - * - * @param installation - The GitHub installation for which to retrieve settings. - * @returns A promise that resolves to the integration setting or rejects with an error. - */ - private async getInstallationSetting( - installation: IGithubInstallation - ): Promise { - try { - const installation_id = installation.id; - // Retrieve the integration setting associated with the GitHub installation. - return await this._commandBus.execute( - new IntegrationSettingGetCommand({ - where: { - settingsName: GithubPropertyMapEnum.INSTALLATION_ID, - settingsValue: installation_id, - isActive: true, - isArchived: false, - integration: { - isActive: true, - isArchived: false, - } - }, - relations: { - integration: { - settings: true, - entitySettings: { - tiedEntities: true - } - } - } - }) - ); - } catch (error) { - this.logger.error(`Failed to fetch GitHub installation setting: ${installation?.id}`, error.message); - } - } -} diff --git a/packages/core/src/integration/github/repository/dto/update-github-repository.dto.ts b/packages/core/src/integration/github/repository/dto/update-github-repository.dto.ts deleted file mode 100644 index 0eb9f29949c..00000000000 --- a/packages/core/src/integration/github/repository/dto/update-github-repository.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IOrganizationGithubRepositoryUpdateInput } from "@gauzy/contracts"; -import { IntersectionType, PickType } from "@nestjs/swagger"; -import { TenantOrganizationBaseDTO } from "core/dto"; -import { OrganizationGithubRepository } from "../github-repository.entity"; - -/** - * A Data Transfer Object (DTO) for updating an organization's GitHub repository. - * This DTO is used to specify which properties of the repository should be updated. - * It combines properties from different sources to define the structure for the update. - */ -export class UpdateGithubRepositoryDTO extends IntersectionType( - TenantOrganizationBaseDTO, - PickType(OrganizationGithubRepository, ['hasSyncEnabled']) -) implements IOrganizationGithubRepositoryUpdateInput { } diff --git a/packages/core/src/integration/github/repository/github-repository.entity.ts b/packages/core/src/integration/github/repository/github-repository.entity.ts deleted file mode 100644 index 5a3f0be7a24..00000000000 --- a/packages/core/src/integration/github/repository/github-repository.entity.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { JoinColumn, RelationId } from 'typeorm'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; -import { IIntegrationTenant, IOrganizationGithubRepository, IOrganizationGithubRepositoryIssue, IOrganizationProject } from '@gauzy/contracts'; -import { IntegrationTenant, OrganizationGithubRepositoryIssue, OrganizationProject, TenantOrganizationBaseEntity } from '../../../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne, MultiORMOneToMany } from '../../../core/decorators/entity'; -import { MikroOrmOrganizationGithubRepositoryRepository } from './repository/mikro-orm-organization-github-repository.repository'; - -@MultiORMEntity('organization_github_repository', { mikroOrmRepository: () => MikroOrmOrganizationGithubRepositoryRepository }) -export class OrganizationGithubRepository extends TenantOrganizationBaseEntity implements IOrganizationGithubRepository { - - @ApiProperty({ type: () => Number }) - @IsNotEmpty() - @IsNumber() - @ColumnIndex() - @MultiORMColumn() - repositoryId: number; - - @ApiProperty({ type: () => String }) - @IsNotEmpty() - @IsString() - @ColumnIndex() - @MultiORMColumn() - name: string; - - @ApiProperty({ type: () => String }) - @IsNotEmpty() - @IsString() - @ColumnIndex() - @MultiORMColumn() - fullName: string; - - @ApiProperty({ type: () => String }) - @IsNotEmpty() - @IsString() - @ColumnIndex() - @MultiORMColumn() - owner: string; - - @ApiPropertyOptional({ type: () => Number }) - @IsNotEmpty() - @IsNumber() - @ColumnIndex() - @MultiORMColumn({ nullable: true }) - issuesCount: number; - - @ApiPropertyOptional({ type: () => Boolean }) - @IsOptional() - @IsBoolean() - @ColumnIndex() - @MultiORMColumn({ nullable: true, default: true }) - hasSyncEnabled: boolean; - - @ApiPropertyOptional({ type: () => Boolean }) - @IsOptional() - @IsBoolean() - @ColumnIndex() - @MultiORMColumn({ nullable: true, default: false }) - private: boolean; - - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @ColumnIndex() - @MultiORMColumn({ nullable: true }) - status: string; - - /* - |-------------------------------------------------------------------------- - | @ManyToOne - |-------------------------------------------------------------------------- - */ - - /** What integration tenant sync to */ - @ApiProperty({ type: () => IntegrationTenant }) - @MultiORMManyToOne(() => IntegrationTenant, { - /** Indicates if relation column value can be nullable or not. */ - nullable: true, - - /** Database cascade action on delete. */ - onDelete: 'CASCADE' - }) - @JoinColumn() - integration: IIntegrationTenant; - - @ApiProperty({ type: () => String }) - @IsUUID() - @RelationId((it: OrganizationGithubRepository) => it.integration) - @ColumnIndex() - @MultiORMColumn({ nullable: true, relationId: true }) - integrationId: IIntegrationTenant['id']; - - /* - |-------------------------------------------------------------------------- - | @OneToMany - |-------------------------------------------------------------------------- - */ - - /** Repository Sync Organization Projects */ - @MultiORMOneToMany(() => OrganizationProject, (it) => it.repository, { - cascade: true - }) - projects?: IOrganizationProject[]; - - /** Repository Sync Organization Projects */ - @MultiORMOneToMany(() => OrganizationGithubRepositoryIssue, (it) => it.repository, { - cascade: true - }) - issues?: IOrganizationGithubRepositoryIssue[]; -} diff --git a/packages/core/src/integration/github/repository/issue/github-repository-issue.entity.ts b/packages/core/src/integration/github/repository/issue/github-repository-issue.entity.ts deleted file mode 100644 index 7879cade10e..00000000000 --- a/packages/core/src/integration/github/repository/issue/github-repository-issue.entity.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { JoinColumn, RelationId } from 'typeorm'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; -import { IOrganizationGithubRepository, IOrganizationGithubRepositoryIssue } from '@gauzy/contracts'; -import { TenantOrganizationBaseEntity } from '../../../../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from '../../../../core/decorators/entity'; -import { OrganizationGithubRepository } from './../github-repository.entity'; -import { MikroOrmOrganizationGithubRepositoryIssueRepository } from './repository/mikro-orm-github-repository-issue.repository'; - -@MultiORMEntity('organization_github_repository_issue', { mikroOrmRepository: () => MikroOrmOrganizationGithubRepositoryIssueRepository }) -export class OrganizationGithubRepositoryIssue extends TenantOrganizationBaseEntity implements IOrganizationGithubRepositoryIssue { - - @ApiProperty({ type: () => Number }) - @IsNotEmpty() - @IsNumber() - @ColumnIndex() - @MultiORMColumn() - issueId: number; - - @ApiProperty({ type: () => Number }) - @IsNotEmpty() - @IsString() - @ColumnIndex() - @MultiORMColumn() - issueNumber: number; - - /* - |-------------------------------------------------------------------------- - | @ManyToOne - |-------------------------------------------------------------------------- - */ - - /** - * Organization Github Repository - */ - @MultiORMManyToOne(() => OrganizationGithubRepository, { - /** Indicates if relation column value can be nullable or not. */ - nullable: true, - - /** Database cascade action on delete. */ - onDelete: 'SET NULL' - }) - @JoinColumn() - repository?: IOrganizationGithubRepository; - - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsUUID() - @RelationId((it: OrganizationGithubRepositoryIssue) => it.repository) - @ColumnIndex() - @MultiORMColumn({ nullable: true, relationId: true }) - repositoryId?: IOrganizationGithubRepository['id']; -} diff --git a/packages/core/src/integration/hubstaff/hubstaff-authorization.controller.ts b/packages/core/src/integration/hubstaff/hubstaff-authorization.controller.ts deleted file mode 100644 index bc8d43cde6f..00000000000 --- a/packages/core/src/integration/hubstaff/hubstaff-authorization.controller.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 { IHubstaffConfig, Public } from '@gauzy/common'; -import { IntegrationEnum } from '@gauzy/contracts'; - -@ApiTags('Hubstaff Integrations') -@Public() -@Controller() -export class HubstaffAuthorizationController { - constructor( - private readonly _config: ConfigService - ) { } - - /** - * 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 hubstaffIntegrationCallback( - @Query() query: any, - @Res() response: Response - ) { - try { - // 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); - } - - /** 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) { - // 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.controller.ts b/packages/core/src/integration/hubstaff/hubstaff.controller.ts deleted file mode 100644 index f414ab820c7..00000000000 --- a/packages/core/src/integration/hubstaff/hubstaff.controller.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Controller, Post, Body, Get, Param, UseGuards, Query } from '@nestjs/common'; -import { - IIntegrationTenant, - IHubstaffOrganization, - IHubstaffProject, - IIntegrationMap, - IIntegrationSetting, - PermissionsEnum, - ICreateHubstaffIntegrationInput, - IOrganization -} from '@gauzy/contracts'; -import { ApiTags } from '@nestjs/swagger'; -import { Permissions } from 'shared/decorators'; -import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; -import { UUIDValidationPipe } from '../../shared/pipes'; -import { HubstaffService } from './hubstaff.service'; - -@ApiTags('Hubstaff Integrations') -@UseGuards(TenantPermissionGuard, PermissionGuard) -@Permissions(PermissionsEnum.INTEGRATION_VIEW) -@Controller() -export class HubstaffController { - constructor( - private readonly _hubstaffService: HubstaffService, - ) { } - - /** - * - * - * @param integrationId - * @returns - */ - @Get('/token/:integrationId') - async getHubstaffTokenByIntegration( - @Param('integrationId', UUIDValidationPipe) integrationId: IIntegrationTenant['id'] - ): Promise { - return await this._hubstaffService.getHubstaffToken(integrationId); - } - - /** - * - * @param integrationId - * @returns - */ - @Get('/refresh-token/:integrationId') - async refreshHubstaffTokenByIntegration( - @Param('integrationId', UUIDValidationPipe) integrationId: IIntegrationTenant['id'] - ): Promise { - return await this._hubstaffService.refreshToken(integrationId); - } - - /** - * - * @param body - * @returns - */ - @Post('/integration') - async create( - @Body() body: ICreateHubstaffIntegrationInput - ): Promise { - return await this._hubstaffService.addIntegration(body); - } - - /** - * - * @param integrationId - * @param body - * @returns - */ - @Get('/organizations') - async getOrganizations( - @Query('token') token: string, - ): Promise { - return await this._hubstaffService.getOrganizations(token); - } - - /** - * - * @param organizationId - * @param body - * @returns - */ - @Get('/projects/:organizationId') - async getProjects( - @Param('organizationId') organizationId: IOrganization['id'], - @Query('token') token: string - ): Promise { - return await this._hubstaffService.fetchOrganizationProjects({ - token, - organizationId - }); - } - - /** - * - * @param integrationId - * @param body - * @returns - */ - @Post('/sync-projects') - async syncProjects( - @Body() input: any - ): Promise { - return await this._hubstaffService.syncProjects(input); - } - - /** - * - * @param integrationId - * @param body - * @returns - */ - @Post('/sync-organizations') - async syncOrganizations( - @Body() input: any - ): Promise { - return await this._hubstaffService.syncOrganizations(input); - } - - /** - * - * @param integrationId - * @param body - * @returns - */ - @Post('/auto-sync/:integrationId') - async autoSync( - @Param('integrationId', UUIDValidationPipe) integrationId: string, - @Body() body - ): Promise { - return await this._hubstaffService.autoSync({ - ...body, - integrationId - }); - } -} diff --git a/packages/core/src/integration/hubstaff/hubstaff.module.ts b/packages/core/src/integration/hubstaff/hubstaff.module.ts deleted file mode 100644 index a2de288aa2c..00000000000 --- a/packages/core/src/integration/hubstaff/hubstaff.module.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import { HttpModule } from '@nestjs/axios'; -import { CqrsModule } from '@nestjs/cqrs'; -import { HUBSTAFF_API_URL } from '@gauzy/integration-hubstaff'; -import { UserModule } from 'user/user.module'; -import { RoleModule } from 'role/role.module'; -import { RolePermissionModule } from '../../role-permission/role-permission.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'; -import { OrganizationProjectModule } from 'organization-project/organization-project.module'; -import { ScreenshotModule } from 'time-tracking/screenshot/screenshot.module'; -import { HubstaffService } from './hubstaff.service'; -import { HubstaffController } from './hubstaff.controller'; -import { HubstaffAuthorizationController } from './hubstaff-authorization.controller'; - -@Module({ - imports: [ - HttpModule.register({ baseURL: HUBSTAFF_API_URL }), - UserModule, - RoleModule, - OrganizationModule, - RolePermissionModule, - OrganizationProjectModule, - forwardRef(() => IntegrationModule), - IntegrationTenantModule, - IntegrationSettingModule, - IntegrationEntitySettingModule, - IntegrationEntitySettingTiedModule, - IntegrationMapModule, - ScreenshotModule, - CqrsModule - ], - controllers: [ - HubstaffAuthorizationController, - HubstaffController - ], - providers: [ - HubstaffService - ] -}) -export class HubstaffModule { } diff --git a/packages/core/src/integration/hubstaff/hubstaff.service.ts b/packages/core/src/integration/hubstaff/hubstaff.service.ts deleted file mode 100644 index d85ebb4e484..00000000000 --- a/packages/core/src/integration/hubstaff/hubstaff.service.ts +++ /dev/null @@ -1,1384 +0,0 @@ -import { - Injectable, - BadRequestException, - HttpException, - HttpStatus -} from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { AxiosError, AxiosResponse } from 'axios'; -import { CommandBus } from '@nestjs/cqrs'; -import { DeepPartial } from 'typeorm'; -import { map, catchError, switchMap } from 'rxjs/operators'; -import * as moment from 'moment'; -import { environment as env } from '@gauzy/config'; -import { isEmpty, isNotEmpty, isObject } from '@gauzy/common'; -import { - IIntegrationTenant, - IntegrationEnum, - IntegrationEntity, - IIntegrationMap, - IIntegrationSetting, - RolesEnum, - TimeLogType, - ContactType, - CurrenciesEnum, - ProjectBillingEnum, - TimeLogSourceEnum, - IHubstaffOrganization, - IHubstaffProject, - IIntegrationEntitySetting, - IDateRangeActivityFilter, - ComponentLayoutStyleEnum, - ActivityType, - IDateRange, - OrganizationProjectBudgetTypeEnum, - OrganizationContactBudgetTypeEnum, - IHubstaffProjectsResponse, - IHubstaffOrganizationsResponse, - IHubstaffProjectResponse, - IHubstaffTimeSlotActivity, - IActivity, - IHubstaffLogFromTimeSlots, - ICreateHubstaffIntegrationInput -} from '@gauzy/contracts'; -import { - DEFAULT_ENTITY_SETTINGS, - HUBSTAFF_AUTHORIZATION_URL, - PROJECT_TIED_ENTITIES -} from '@gauzy/integration-hubstaff'; -import { firstValueFrom, lastValueFrom } from 'rxjs'; -import { RequestContext } from 'core/context'; -import { mergeOverlappingDateRanges } from 'core/utils'; -import { IntegrationSettingService } from 'integration-setting/integration-setting.service'; -import { OrganizationContactCreateCommand } from 'organization-contact/commands'; -import { EmployeeCreateCommand, EmployeeGetCommand } from 'employee/commands'; -import { RoleService } from 'role/role.service'; -import { OrganizationService } from 'organization/organization.service'; -import { UserService } from 'user/user.service'; -import { IntegrationMapService } from 'integration-map/integration-map.service'; -import { - IntegrationMapSyncActivityCommand, - IntegrationMapSyncEntityCommand, - IntegrationMapSyncOrganizationCommand, - IntegrationMapSyncProjectCommand, - IntegrationMapSyncScreenshotCommand, - IntegrationMapSyncTaskCommand, - IntegrationMapSyncTimeLogCommand, - IntegrationMapSyncTimeSlotCommand -} from 'integration-map/commands'; -import { IntegrationTenantService } from 'integration-tenant/integration-tenant.service'; -import { IntegrationTenantUpdateOrCreateCommand } from 'integration-tenant/commands'; -import { IntegrationService } from 'integration/integration.service'; - -@Injectable() -export class HubstaffService { - constructor( - private readonly _httpService: HttpService, - private readonly _integrationTenantService: IntegrationTenantService, - private readonly _integrationSettingService: IntegrationSettingService, - private readonly _integrationMapService: IntegrationMapService, - private readonly _roleService: RoleService, - private readonly _organizationService: OrganizationService, - private readonly _userService: UserService, - private readonly _commandBus: CommandBus, - private readonly _integrationService: IntegrationService - ) { } - - async fetchIntegration(url: string, token: string): Promise { - const headers = { - Authorization: `Bearer ${token}` - }; - return firstValueFrom( - this._httpService.get(url, { headers }).pipe( - catchError((error: AxiosError) => { - const response: AxiosResponse = error.response; - console.log('Error while hubstaff API: %s', response); - - /** Handle hubstaff http exception */ - throw new HttpException({ message: error.message, error }, response.status); - }), - map( - (response: AxiosResponse) => response.data - ) - ) - ); - } - - async refreshToken(integrationId) { - const { - items: settings - } = await this._integrationSettingService.findAll({ - where: { - integration: { id: integrationId } - } - }); - const headers = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - const urlParams = new URLSearchParams(); - - const { client_id, client_secret, refresh_token } = settings.reduce( - (prev, current) => { - return { - ...prev, - client_id: - current.settingsName === 'client_id' - ? current.settingsValue - : prev.client_id, - client_secret: - current.settingsName === 'client_secret' - ? current.settingsValue - : prev.client_secret, - refresh_token: - current.settingsName === 'refresh_token' - ? current.settingsValue - : prev.refresh_token - }; - }, - { - client_id: '', - client_secret: '', - refresh_token: '' - } - ); - urlParams.append('grant_type', 'refresh_token'); - urlParams.append('refresh_token', refresh_token); - urlParams.append('client_id', client_id); - urlParams.append('client_secret', client_secret); - - try { - const tokens$ = this._httpService.post(`${HUBSTAFF_AUTHORIZATION_URL}/access_tokens`, urlParams, { - headers - }).pipe( - map( - (response: AxiosResponse) => response.data - ) - ); - const tokens = await lastValueFrom(tokens$); - const settingsDto = settings.map((setting) => { - if (setting.settingsName === 'access_token') { - setting.settingsValue = tokens.access_token; - } - - if (setting.settingsName === 'refresh_token') { - setting.settingsValue = tokens.refresh_token; - } - - return setting; - }) as DeepPartial; - - await this._integrationSettingService.create(settingsDto); - return tokens; - } catch (error) { - throw new BadRequestException(error); - } - } - - - async getHubstaffToken(integrationId): Promise { - const { - record: integrationSetting - } = await this._integrationSettingService.findOneOrFailByOptions({ - where: { - integration: { id: integrationId }, - settingsName: 'access_token' - } - }); - return integrationSetting; - } - - async addIntegration( - body: ICreateHubstaffIntegrationInput - ): Promise { - const tenantId = RequestContext.currentTenantId(); - const { client_id, client_secret, code, redirect_uri, organizationId } = body; - - const urlParams = new URLSearchParams(); - urlParams.append('client_id', client_id); - urlParams.append('code', code); - urlParams.append('grant_type', 'authorization_code'); - urlParams.append('redirect_uri', redirect_uri); - urlParams.append('client_secret', client_secret); - - /** */ - const integration = await this._integrationService.findOneByOptions({ - where: { - provider: IntegrationEnum.HUBSTAFF - } - }); - - const tiedEntities = PROJECT_TIED_ENTITIES.map(entity => ({ - ...entity, - organizationId, - tenantId - })); - - const entitySettings = DEFAULT_ENTITY_SETTINGS.map((settingEntity) => { - if (settingEntity.entity === IntegrationEntity.PROJECT) { - return { - ...settingEntity, - tiedEntities - }; - } - return { - ...settingEntity, - organizationId, - tenantId - }; - }) as IIntegrationEntitySetting[]; - - - 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 IntegrationTenantUpdateOrCreateCommand({ - name: IntegrationEnum.HUBSTAFF, - integration: { - provider: IntegrationEnum.HUBSTAFF - }, - tenantId, - organizationId, - }, { - name: IntegrationEnum.HUBSTAFF, - integration, - organizationId, - tenantId, - entitySettings: entitySettings, - settings: [ - { - settingsName: 'client_id', - settingsValue: client_id - }, - { - settingsName: 'client_secret', - settingsValue: client_secret - }, - { - settingsName: 'access_token', - settingsValue: data.access_token - }, - { - settingsName: 'refresh_token', - settingsValue: data.refresh_token - } - ].map((setting) => ({ - ...setting, - tenantId, - organizationId, - })) - }) - )), - catchError((err) => { - throw new BadRequestException(err); - }) - ); - - return await lastValueFrom(tokens$); - } - - /*** - * Get all organizations - */ - async getOrganizations(token: string): Promise { - const { organizations } = await this.fetchIntegration('organizations', token); - return organizations; - } - - /* - * Fetch all organization projects - */ - async fetchOrganizationProjects({ - organizationId, - token - }): Promise { - const { projects } = await this.fetchIntegration(`organizations/${organizationId}/projects?status=all&include=clients`, token); - return projects; - } - - /** - * - * @param param0 - * @returns - */ - async syncProjects({ - integrationId, - organizationId, - projects, - token - }): Promise { - try { - const tenantId = RequestContext.currentTenantId(); - return await Promise.all( - await projects.map( - async ({ sourceId }) => { - const { project } = await this.fetchIntegration(`projects/${sourceId}`, token); - - /** Third Party Organization Project Map */ - return await this._commandBus.execute( - new IntegrationMapSyncProjectCommand({ - entity: { - name: project.name, - description: project.description, - billable: project.billable, - public: true, - billing: ProjectBillingEnum.RATE, - currency: env.defaultCurrency as CurrenciesEnum, - organizationId, - tenantId, - /** Set Project Budget Here */ - ...(project.budget - ? { - budgetType: project.budget.type || OrganizationProjectBudgetTypeEnum.COST, - startDate: project.budget.start_date || null, - budget: project.budget[project.budget.type || OrganizationProjectBudgetTypeEnum.COST] - } - : {}), - }, - sourceId, - integrationId, - organizationId, - tenantId - }) - ); - } - ) - ) - } catch (error) { - console.log(`Error while syncing ${IntegrationEntity.PROJECT} entity for organization (${organizationId}): %s`, error?.message); - throw new HttpException({ message: error?.message, error }, HttpStatus.BAD_REQUEST); - } - } - - /** - * - * @param param0 - * @returns - */ - async syncOrganizations({ - integrationId, - organizationId, - organizations, - token - }): Promise { - try { - const tenantId = RequestContext.currentTenantId(); - return await Promise.all( - await organizations.map( - async ({ sourceId }) => { - const { organization } = await this.fetchIntegration(`organizations/${sourceId}`, token); - /** Third Party Organization Map */ - return await this._commandBus.execute( - new IntegrationMapSyncOrganizationCommand({ - entity: { - name: organization.name, - isActive: organization.status == 'active', - currency: env.defaultCurrency as CurrenciesEnum - }, - sourceId, - integrationId, - organizationId, - tenantId - }) - ); - } - ) - ); - } catch (error) { - console.log(`Error while syncing ${IntegrationEntity.ORGANIZATION} entity (${organizationId}): %s`, error?.message); - throw new HttpException({ message: error?.message, error }, HttpStatus.BAD_REQUEST); - } - } - - async syncClients({ - integrationId, - organizationId, - clients - }): Promise { - try { - return await Promise.all( - await clients.map( - async ({ id, name, emails, phone, budget = {} as any }) => { - const { record } = await this._integrationMapService.findOneOrFailByOptions({ - where: { - sourceId: id, - entity: IntegrationEntity.CLIENT, - organizationId - } - }); - if (record) { - return record; - } - - /** - * Set Client Budget Here - */ - let clientBudget = {}; - if (isNotEmpty(budget)) { - clientBudget['budgetType'] = budget.type || OrganizationContactBudgetTypeEnum.COST; - clientBudget['budget'] = budget[clientBudget['budgetType']]; - } - - const gauzyClient = await this._commandBus.execute( - new OrganizationContactCreateCommand({ - name, - organizationId, - primaryEmail: emails[0], - primaryPhone: phone, - contactType: ContactType.CLIENT, - ...clientBudget - }) - ); - return await this._commandBus.execute( - new IntegrationMapSyncEntityCommand({ - gauzyId: gauzyClient.id, - integrationId, - sourceId: id, - entity: IntegrationEntity.CLIENT, - organizationId - }) - ); - } - ) - ); - } catch (error) { - throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.CLIENT}`); - } - } - - /* - * Sync screenshot using timeslot - */ - async syncScreenshots({ - integrationId, - screenshots, - token, - organizationId - }): Promise { - try { - let integratedScreenshots: IIntegrationMap[] = []; - for await (const screenshot of screenshots) { - const { id, user_id } = screenshot; - const employee = await this._getEmployeeByHubstaffUserId( - user_id, - token, - integrationId, - organizationId - ); - integratedScreenshots.push( - await this._commandBus.execute( - new IntegrationMapSyncScreenshotCommand({ - entity: { - employeeId: employee ? employee.gauzyId : null, - ...screenshot - }, - sourceId: id, - integrationId, - organizationId - }) - ) - ); - } - return integratedScreenshots; - } catch (error) { - throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.SCREENSHOT}`); - } - } - - async syncTasks({ - integrationId, - projectId, - tasks, - organizationId - }): Promise { - try { - const tenantId = RequestContext.currentTenantId(); - const creatorId = RequestContext.currentUserId(); - - return await Promise.all( - await tasks.map( - async ({ summary: title, details = null, id, status, due_at }) => { - if (!due_at) { - due_at = new Date(moment().add(2, 'week').format('YYYY-MM-DD HH:mm:ss')); - } - - // Step 1: Execute a command to initiate the synchronization process - const triggeredEvent = false; - return await this._commandBus.execute( - new IntegrationMapSyncTaskCommand({ - entity: { - title, - projectId, - description: details, - status: status.charAt(0).toUpperCase() + status.slice(1), - creatorId, - dueDate: due_at, - organizationId, - tenantId - }, - sourceId: id, - integrationId, - organizationId, - tenantId - }, triggeredEvent) - ); - } - ) - ); - } catch (error) { - throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.TASK}`); - } - } - - private async _getEmployeeByHubstaffUserId( - user_id: string, - token: string, - integrationId: string, - organizationId: string - ) { - try { - const tenantId = RequestContext.currentTenantId(); - return await this._integrationMapService.findOneByOptions({ - where: { - sourceId: user_id, - entity: IntegrationEntity.EMPLOYEE, - organizationId, - tenantId - } - }); - } catch (error) { - return await this._handleEmployee({ - user_id, - token, - integrationId, - organizationId - }); - } - } - - /** - * Map worked timeslot activity - * - * @param timeSlots - * @returns - */ - async syncTimeSlots( - integrationId: string, - organizationId: string, - employee: IIntegrationMap, - timeSlots: IHubstaffTimeSlotActivity[], - ): Promise { - try { - return timeSlots - .filter(async (timeslot: IHubstaffTimeSlotActivity) => { - return !!await this._commandBus.execute( - new IntegrationMapSyncTimeSlotCommand({ - entity: { - ...timeslot, - employeeId: employee.gauzyId - }, - sourceId: (timeslot.id).toString(), - integrationId, - organizationId - }) - ); - }) - .map( - ({ keyboard, mouse, overall, tracked, time_slot }) => ({ - keyboard, - mouse, - overall, - duration: tracked, - startedAt: time_slot - }) - ); - } catch (error) { - throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.TIME_SLOT}`); - } - } - - async syncTimeLogs( - timeLogs: any, - token: string, - integrationId: string, - organizationId: string, - projectId: string - ): Promise { - try { - let integratedTimeLogs: IIntegrationMap[] = []; - const tenantId = RequestContext.currentTenantId(); - - for await (const timeLog of timeLogs) { - const { id, user_id, task_id, logType, startedAt, stoppedAt, timeSlots } = timeLog; - const employee = await this._getEmployeeByHubstaffUserId( - user_id, - token, - integrationId, - organizationId - ); - const { record } = await this._integrationMapService.findOneOrFailByOptions({ - where: { - sourceId: task_id, - entity: IntegrationEntity.TASK, - organizationId, - tenantId - } - }); - const syncTimeSlots = await this.syncTimeSlots( - integrationId, - organizationId, - employee, - timeSlots - ); - integratedTimeLogs.push( - await this._commandBus.execute( - new IntegrationMapSyncTimeLogCommand({ - entity: { - projectId, - employeeId: employee.gauzyId, - taskId: record ? record.gauzyId : null, - logType, - startedAt, - stoppedAt, - source: TimeLogSourceEnum.HUBSTAFF, - organizationId, - tenantId, - timeSlots: syncTimeSlots - }, - sourceId: id, - integrationId, - organizationId - }) - ) - ); - } - return integratedTimeLogs; - } catch (error) { - throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.TIME_LOG}`); - } - } - - async syncEmployee({ integrationId, user, organizationId }) { - try { - const tenantId = RequestContext.currentTenantId(); - const { record } = await this._userService.findOneOrFailByOptions({ - where: { - email: user.email, - tenantId - } - }); - let employee; - if (record) { - employee = await this._commandBus.execute( - new EmployeeGetCommand({ where: { userId: record.id } }) - ); - } else { - const [role, organization] = await Promise.all([ - await this._roleService.findOneByOptions({ - where: { - name: RolesEnum.EMPLOYEE, - tenantId - } - }), - await this._organizationService.findOneByOptions({ - where: { - id: organizationId, - tenantId - } - }) - ]); - const [firstName, lastName] = user.name.split(' '); - const isActive = user.status === 'active' ? true : false; - employee = await this._commandBus.execute( - new EmployeeCreateCommand({ - user: { - email: user.email, - firstName, - lastName, - role, - tags: null, - tenantId, - preferredComponentLayout: ComponentLayoutStyleEnum.TABLE, - thirdPartyId: user.id - }, - password: env.defaultIntegratedUserPass, - organization, - startedWorkOn: new Date( - moment().format('YYYY-MM-DD HH:mm:ss') - ), - isActive, - tenantId - }) - ); - } - return await this._commandBus.execute( - new IntegrationMapSyncEntityCommand({ - gauzyId: employee.id, - integrationId, - sourceId: user.id, - entity: IntegrationEntity.EMPLOYEE, - organizationId - }) - ); - } catch (error) { - throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.EMPLOYEE}`); - } - } - - private async _handleEmployee({ - user_id, - integrationId, - token, - organizationId - }) { - try { - const { user } = await this.fetchIntegration( - `users/${user_id}`, - token - ); - return await this.syncEmployee({ - integrationId, - user, - organizationId - }); - } catch (error) { - throw new BadRequestException(error, `Can\'t handle ${IntegrationEntity.EMPLOYEE}`); - } - } - - private async _handleProjects( - sourceId: string, - integrationId: string, - gauzyId: string, - token: string - ) { - try { - const { projects } = await this.fetchIntegration( - `organizations/${sourceId}/projects?status=all`, - token - ); - const projectMap = projects.map(({ name, id, billable, description }) => ({ - name, - sourceId: id, - billable, - description - }) - ); - return await this.syncProjects({ - integrationId, - organizationId: gauzyId, - projects: projectMap, - token - }); - } catch (error) { - throw new BadRequestException(`Can\'t handle ${IntegrationEntity.PROJECT}`); - } - } - - private async _handleClients( - sourceId: string, - integrationId: string, - gauzyId: string, - token: string - ) { - try { - const { clients } = await this.fetchIntegration( - `organizations/${sourceId}/clients?status=active`, - token - ); - return await this.syncClients({ - integrationId, - organizationId: gauzyId, - clients - }); - } catch (error) { - throw new BadRequestException(error, `Can\'t handle ${IntegrationEntity.CLIENT}`); - } - } - - private async _handleTasks(projectsMap, integrationId, token, gauzyId) { - try { - const tasksMap = await Promise.all( - projectsMap.map(async (project) => { - const { tasks } = await this.fetchIntegration(`projects/${project.sourceId}/tasks`, token); - return await this.syncTasks({ - integrationId, - tasks, - projectId: project.gauzyId, - organizationId: gauzyId - }); - }) - ); - return tasksMap; - } catch (error) { - throw new BadRequestException(error, `Can\'t handle ${IntegrationEntity.TASK}`); - } - } - - /* - * Sync with database urls activities - */ - async syncUrlActivities({ - integrationId, - projectId, - activities, - token, - organizationId - }): Promise { - try { - const tenantId = RequestContext.currentTenantId(); - return await Promise.all( - await activities.map( - async ({ id, site, tracked, user_id, time_slot, task_id }) => { - const time = moment(time_slot).format('HH:mm:ss'); - const date = moment(time_slot).format('YYYY-MM-DD'); - - const employee = await this._getEmployeeByHubstaffUserId( - user_id, - token, - integrationId, - organizationId - ); - const { record: task } = await this._integrationMapService.findOneOrFailByOptions({ - where: { - sourceId: task_id, - entity: IntegrationEntity.TASK, - organizationId, - tenantId - } - }); - const entity: IActivity = { - title: site, - duration: tracked, - type: ActivityType.URL, - time, - date, - projectId, - employeeId: employee ? employee.gauzyId : null, - taskId: task ? task.gauzyId : null, - organizationId, - activityTimestamp: time_slot - }; - return await this._commandBus.execute( - new IntegrationMapSyncActivityCommand({ - entity, - sourceId: id, - integrationId, - organizationId - }) - ); - } - ) - ); - } catch (error) { - throw new BadRequestException(error, `Can\'t sync URL ${IntegrationEntity.ACTIVITY}`); - } - } - - /* - * auto sync for urls activities for separate project - */ - private async _handleUrlActivities( - projectsMap: IIntegrationMap[], - integrationId: string, - token: string, - organizationId: string, - dateRange: IDateRangeActivityFilter - ) { - try { - const start = moment(dateRange.start).format('YYYY-MM-DD'); - const end = moment(dateRange.end).format('YYYY-MM-DD'); - const pageLimit = 500; - - const urlActivitiesMapped = await Promise.all( - projectsMap.map(async (project) => { - const { gauzyId, sourceId } = project; - const syncedActivities = { - urlActivities: [] - }; - - let stillRecordsAvailable = true; - let nextPageStartId = null; - - while (stillRecordsAvailable) { - let url = `projects/${sourceId}/url_activities?page_limit=${pageLimit}&time_slot[start]=${start}&time_slot[stop]=${end}`; - if (nextPageStartId) { - url += `&page_start_id=${nextPageStartId}`; - } - - const { - urls, - pagination = {} - } = await this.fetchIntegration(url, token); - - if ( - pagination && - pagination.hasOwnProperty('next_page_start_id') - ) { - const { next_page_start_id } = pagination; - nextPageStartId = next_page_start_id; - stillRecordsAvailable = true; - } else { - nextPageStartId = null; - stillRecordsAvailable = false; - } - syncedActivities.urlActivities.push(urls); - } - - const activities = [].concat.apply( - [], - syncedActivities.urlActivities - ); - return await this.syncUrlActivities({ - integrationId, - projectId: gauzyId, - activities, - token, - organizationId - }); - }) - ); - return urlActivitiesMapped; - } catch (error) { - throw new BadRequestException(error, `Can\'t handle URL ${IntegrationEntity.ACTIVITY}`); - } - } - - /* - * Sync with database application activities - */ - async syncAppActivities({ - integrationId, - projectId, - activities, - token, - organizationId - }): Promise { - try { - const tenantId = RequestContext.currentTenantId(); - return await Promise.all( - await activities.map( - async ({ id, name, tracked, user_id, time_slot, task_id }) => { - const time = moment(time_slot).format('HH:mm:ss'); - const date = moment(time_slot).format('YYYY-MM-DD'); - - const employee = await this._getEmployeeByHubstaffUserId( - user_id, - token, - integrationId, - organizationId - ); - const { record: task } = await this._integrationMapService.findOneOrFailByOptions({ - where: { - sourceId: task_id, - entity: IntegrationEntity.TASK, - organizationId, - tenantId - } - }); - const entity: IActivity = { - title: name, - duration: tracked, - type: ActivityType.APP, - time, - date, - projectId, - employeeId: employee ? employee.gauzyId : null, - taskId: task ? task.gauzyId : null, - organizationId, - activityTimestamp: time_slot - }; - return await this._commandBus.execute( - new IntegrationMapSyncActivityCommand({ - entity, - sourceId: id, - integrationId, - organizationId - }) - ); - } - ) - ); - } catch (error) { - throw new BadRequestException(error, `Can\'t sync APP ${IntegrationEntity.ACTIVITY}`); - } - } - - /* - * auto sync for application activities for separate project - */ - private async _handleAppActivities( - projectsMap: IIntegrationMap[], - integrationId: string, - token: string, - organizationId: string, - dateRange: IDateRangeActivityFilter - ) { - try { - const start = moment(dateRange.start).format('YYYY-MM-DD'); - const end = moment(dateRange.end).format('YYYY-MM-DD'); - const pageLimit = 500; - - const appActivitiesMapped = await Promise.all( - projectsMap.map(async (project) => { - const { gauzyId, sourceId } = project; - const syncedActivities = { - applicationActivities: [] - }; - - let stillRecordsAvailable = true; - let nextPageStartId = null; - - while (stillRecordsAvailable) { - let url = `projects/${sourceId}/application_activities?page_limit=${pageLimit}&time_slot[start]=${start}&time_slot[stop]=${end}`; - if (nextPageStartId) { - url += `&page_start_id=${nextPageStartId}`; - } - - const { - applications, - pagination = {} - } = await this.fetchIntegration(url, token); - - if ( - pagination && - pagination.hasOwnProperty('next_page_start_id') - ) { - const { next_page_start_id } = pagination; - nextPageStartId = next_page_start_id; - stillRecordsAvailable = true; - } else { - nextPageStartId = null; - stillRecordsAvailable = false; - } - syncedActivities.applicationActivities.push( - applications - ); - } - - const activities = [].concat.apply( - [], - syncedActivities.applicationActivities - ); - return await this.syncAppActivities({ - integrationId, - projectId: gauzyId, - activities, - token, - organizationId - }); - }) - ); - return appActivitiesMapped; - } catch (error) { - throw new BadRequestException(error, `Can\'t handle APP ${IntegrationEntity.ACTIVITY}`); - } - } - - private async _handleActivities( - projectsMap: IIntegrationMap[], - integrationId: string, - token: string, - organizationId: string, - dateRange: IDateRangeActivityFilter - ) { - try { - const start = moment(dateRange.start).format('YYYY-MM-DD'); - const end = moment(dateRange.end).format('YYYY-MM-DD'); - - const integratedTimeLogs: IIntegrationMap[] = []; - - for await (const project of projectsMap) { - const { activities } = await this.fetchIntegration( - `projects/${project.sourceId}/activities?time_slot[start]=${start}&time_slot[stop]=${end}`, - token - ); - if (isEmpty(activities)) { - continue; - } - const timeLogs = this.formatLogsFromSlots(activities); - const integratedTimeLogs = await this.syncTimeLogs( - timeLogs, - token, - integrationId, - organizationId, - project.gauzyId - ); - integratedTimeLogs.push(...integratedTimeLogs); - } - return integratedTimeLogs; - } catch (error) { - if (error instanceof HttpException) { - throw new HttpException(error.getResponse(), error.getStatus()); - } - throw new BadRequestException(error, `Can\'t handle ${IntegrationEntity.ACTIVITY}`); - } - } - - /** - * Sync activities screenshots - */ - private async _handleScreenshots( - projectsMap: IIntegrationMap[], - integrationId: string, - token: string, - organizationId: string, - dateRange: IDateRangeActivityFilter - ): Promise { - try { - - const start = moment(dateRange.start).format('YYYY-MM-DD'); - const end = moment(dateRange.end).format('YYYY-MM-DD'); - const pageLimit = 500; - - return await Promise.all( - projectsMap.map(async (project) => { - const { sourceId } = project; - const syncedActivities = { - screenshots: [] - }; - - let stillRecordsAvailable = true; - let nextPageStartId = null; - - while (stillRecordsAvailable) { - let url = `projects/${sourceId}/screenshots?page_limit=${pageLimit}&time_slot[start]=${start}&time_slot[stop]=${end}`; - if (nextPageStartId) { - url += `&page_start_id=${nextPageStartId}`; - } - - const { - screenshots: fetchScreenshots, - pagination = {} - } = await this.fetchIntegration(url, token); - - if ( - pagination && - pagination.hasOwnProperty('next_page_start_id') - ) { - const { next_page_start_id } = pagination; - nextPageStartId = next_page_start_id; - stillRecordsAvailable = true; - } else { - nextPageStartId = null; - stillRecordsAvailable = false; - } - - syncedActivities.screenshots.push(fetchScreenshots); - } - - const screenshots = [].concat.apply( - [], - syncedActivities.screenshots - ); - return await this.syncScreenshots({ - integrationId, - screenshots, - token, - organizationId - }); - }) - ); - } catch (error) { - throw new BadRequestException(error, `Can\'t handle activities ${IntegrationEntity.SCREENSHOT}`); - } - } - - async autoSync({ - integrationId, - gauzyId, - sourceId, - token, - dateRange - }) { - console.log(`${IntegrationEnum.HUBSTAFF} integration start for ${integrationId}`); - /** - * GET organization tenant integration entities settings - */ - const { entitySettings } = await this._integrationTenantService.findOneByIdString(integrationId, { - relations: { - entitySettings: { - tiedEntities: true - } - } - }); - - //entities have depended entity. eg to fetch Task we need Project id or Org id, because our Task entity is related to Project, the relation here is same, we need project id to fetch Tasks - const integratedMaps = await Promise.all( - entitySettings.map(async (setting) => { - switch (setting.entity) { - case IntegrationEntity.PROJECT: - let tasks, activities, screenshots; - const projectsMap: IIntegrationMap[] = await this._handleProjects( - sourceId, - integrationId, - gauzyId, - token - ); - - /** - * Tasks Sync - */ - const taskSetting: IIntegrationEntitySetting = setting.tiedEntities.find( - (res) => res.entity === IntegrationEntity.TASK - ); - if (isObject(taskSetting) && taskSetting.sync) { - tasks = await this._handleTasks( - projectsMap, - integrationId, - token, - gauzyId - ); - } - - /** - * Activity Sync - */ - const activitySetting: IIntegrationEntitySetting = setting.tiedEntities.find( - (res) => res.entity === IntegrationEntity.ACTIVITY - ); - if (isObject(activitySetting) && activitySetting.sync) { - activities = await this._handleActivities( - projectsMap, - integrationId, - token, - gauzyId, - dateRange - ); - activities.application = await this._handleAppActivities( - projectsMap, - integrationId, - token, - gauzyId, - dateRange - ); - activities.urls = await this._handleUrlActivities( - projectsMap, - integrationId, - token, - gauzyId, - dateRange - ); - } - - /** - * Activity Screenshot Sync - */ - const screenshotSetting: IIntegrationEntitySetting = setting.tiedEntities.find( - (res) => res.entity === IntegrationEntity.SCREENSHOT - ); - if (isObject(screenshotSetting) && screenshotSetting.sync) { - screenshots = await this._handleScreenshots( - projectsMap, - integrationId, - token, - gauzyId, - dateRange - ); - } - return { tasks, projectsMap, activities, screenshots }; - case IntegrationEntity.CLIENT: - const clients = await this._handleClients( - sourceId, - integrationId, - gauzyId, - token - ); - return { clients }; - } - }) - ); - console.log(`${IntegrationEnum.HUBSTAFF} integration end for ${integrationId}`); - return integratedMaps; - } - - formatLogsFromSlots(slots: IHubstaffTimeSlotActivity[]) { - if (isEmpty(slots)) { - return; - } - - const range = []; - let i = 0; - while (slots[i]) { - const start = moment(slots[i].starts_at); - const end = moment(slots[i].starts_at).add(slots[i].tracked, 'seconds'); - - range.push({ - start: start.toDate(), - end: end.toDate() - }); - i++; - } - - const timeLogs: Array = []; - const dates: IDateRange[] = mergeOverlappingDateRanges(range); - - if (isNotEmpty(dates)) { - dates.forEach(({ start, end }) => { - let i = 0; - const timeSlots = new Array(); - - while (slots[i]) { - const slotTime = moment(slots[i].starts_at); - if (slotTime.isBetween(moment(start), moment(end), null, '[]')) { - timeSlots.push(slots[i]); - } - i++; - } - - const [activity] = this.getLogsActivityFromSlots(timeSlots); - timeLogs.push({ - startedAt: start, - stoppedAt: end, - timeSlots, - ...activity - }); - }); - } - - return timeLogs; - } - - /** - * GET TimeLogs from Activity TimeSlots - * - * @param timeSlots - * @returns - */ - getLogsActivityFromSlots(timeSlots: IHubstaffTimeSlotActivity[]): IHubstaffLogFromTimeSlots[] { - const timeLogs = timeSlots.reduce((prev, current) => { - const prevLog = prev[current.date]; - return { - ...prev, - [current.date]: prevLog - ? { - id: current.id, - date: current.date, - user_id: prevLog.user_id, - project_id: prevLog.project_id || null, - task_id: prevLog.task_id || null, - // this will take the last chunk(slot), maybe we should allow percentage for this, as one time log can have both manual and tracked - logType: - current.client === 'windows' - ? TimeLogType.TRACKED - : TimeLogType.MANUAL - } - : { - id: current.id, - date: current.date, - user_id: current.user_id, - project_id: current.project_id || null, - task_id: current.task_id || null, - logType: - current.client === 'windows' - ? TimeLogType.TRACKED - : TimeLogType.MANUAL - } - }; - }, {}); - return Object.values(timeLogs); - } -} diff --git a/packages/core/src/integration/index.ts b/packages/core/src/integration/index.ts new file mode 100644 index 00000000000..9b12a465a7c --- /dev/null +++ b/packages/core/src/integration/index.ts @@ -0,0 +1,2 @@ +export * from './integration.module'; +export * from './integration.service'; diff --git a/packages/core/src/integration/integration.module.ts b/packages/core/src/integration/integration.module.ts index 35ce6006d2a..2c304610ed4 100644 --- a/packages/core/src/integration/integration.module.ts +++ b/packages/core/src/integration/integration.module.ts @@ -4,14 +4,12 @@ import { RouterModule } from '@nestjs/core'; import { CqrsModule } from '@nestjs/cqrs'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { RolePermissionModule } from '../role-permission/role-permission.module'; -import { HubstaffModule } from './hubstaff/hubstaff.module'; import { IntegrationType } from './integration-type.entity'; import { Integration } from './integration.entity'; 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({ @@ -21,8 +19,6 @@ import { IntegrationAIModule } from './gauzy-ai/integration-ai.module'; path: '/integration', module: IntegrationModule, children: [ - { path: '/hubstaff', module: HubstaffModule }, - { path: '/github', module: GithubModule }, { path: '/gauzy-ai', module: IntegrationAIModule }, { path: '/', module: IntegrationModule } ] @@ -30,15 +26,13 @@ import { IntegrationAIModule } from './gauzy-ai/integration-ai.module'; ]), TypeOrmModule.forFeature([Integration, IntegrationType]), MikroOrmModule.forFeature([Integration, IntegrationType]), + CqrsModule, IntegrationTenantModule, RolePermissionModule, - forwardRef(() => GithubModule), - forwardRef(() => HubstaffModule), - forwardRef(() => IntegrationAIModule), - CqrsModule + forwardRef(() => IntegrationAIModule) ], controllers: [IntegrationController], providers: [IntegrationService, ...CommandHandlers], exports: [TypeOrmModule, MikroOrmModule, IntegrationService] }) -export class IntegrationModule { } +export class IntegrationModule {} diff --git a/packages/core/src/invoice-estimate-history/dto/invoice-estimate-history.dto.ts b/packages/core/src/invoice-estimate-history/dto/invoice-estimate-history.dto.ts index f2d3bb91004..3ac09e89c0a 100644 --- a/packages/core/src/invoice-estimate-history/dto/invoice-estimate-history.dto.ts +++ b/packages/core/src/invoice-estimate-history/dto/invoice-estimate-history.dto.ts @@ -1,7 +1,7 @@ -import { IInvoice, IUser } from "@gauzy/contracts"; -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator"; -import { TenantOrganizationBaseDTO } from "core/dto"; +import { IInvoice, IUser } from '@gauzy/contracts'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; export abstract class InvoiceEstimateHistoryDTO extends TenantOrganizationBaseDTO { @ApiProperty({ type: () => String, readOnly: true }) diff --git a/packages/core/src/invoice-item/dto/invoice-item.dto.ts b/packages/core/src/invoice-item/dto/invoice-item.dto.ts index da3ababc95b..b5f0ab64773 100644 --- a/packages/core/src/invoice-item/dto/invoice-item.dto.ts +++ b/packages/core/src/invoice-item/dto/invoice-item.dto.ts @@ -1,66 +1,65 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsOptional, IsString, IsNumber, IsNotEmpty, IsBoolean } from "class-validator"; -import { TenantOrganizationBaseDTO } from "core/dto"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString, IsNumber, IsNotEmpty, IsBoolean } from 'class-validator'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; export abstract class InvoiceItemDTO extends TenantOrganizationBaseDTO { + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly description: string; - @ApiProperty({ type: () => String, readOnly: true}) - @IsOptional() - @IsString() - readonly description: string; + @ApiProperty({ type: () => Number, readOnly: true }) + @IsNotEmpty() + @IsNumber() + readonly price: number; - @ApiProperty({ type: () => Number, readOnly: true }) - @IsNotEmpty() - @IsNumber() - readonly price: number; + @ApiProperty({ type: () => Number, readOnly: true }) + @IsNotEmpty() + @IsNumber() + readonly quantity: number; - @ApiProperty({ type: () => Number, readOnly: true }) - @IsNotEmpty() - @IsNumber() - readonly quantity: number; + @ApiProperty({ type: () => Number, readOnly: true }) + @IsNotEmpty() + @IsNumber() + readonly totalValue: number; - @ApiProperty({ type: () => Number, readOnly: true }) - @IsNotEmpty() - @IsNumber() - readonly totalValue: number; + @ApiProperty({ type: () => String, readOnly: true }) + @IsNotEmpty() + @IsString() + readonly invoiceId: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsNotEmpty() - @IsString() - readonly invoiceId: string; + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly taskId: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly taskId: string; + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly employeeId: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly employeeId: string; + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly projectId: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly projectId: string; + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly productId: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly productId: string; + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly expenseId: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly expenseId: string; + @ApiProperty({ type: () => Boolean, readOnly: true }) + @IsOptional() + @IsBoolean() + readonly applyTax: boolean; - @ApiProperty({ type: () => Boolean, readOnly: true }) - @IsOptional() - @IsBoolean() - readonly applyTax: boolean; - - @ApiProperty({ type: () => Boolean, readOnly: true }) - @IsOptional() - @IsBoolean() - readonly applyDiscount: boolean; -} \ No newline at end of file + @ApiProperty({ type: () => Boolean, readOnly: true }) + @IsOptional() + @IsBoolean() + readonly applyDiscount: boolean; +} diff --git a/packages/core/src/keyresult-template/dto/create-keyresult-template.dto.ts b/packages/core/src/keyresult-template/dto/create-keyresult-template.dto.ts index a6b05e1a7e2..511dcf00124 100644 --- a/packages/core/src/keyresult-template/dto/create-keyresult-template.dto.ts +++ b/packages/core/src/keyresult-template/dto/create-keyresult-template.dto.ts @@ -1,7 +1,10 @@ -import { IntersectionType } from "@nestjs/mapped-types"; -import { RelationalGoalKpiTemplateDTO } from "goal-kpi-template/dto"; -import { RelationalGoalTemplateDTO } from "goal-template/dto"; -import { KeyresultTemplateDTO } from "./keyresult-template.dto"; +import { IntersectionType } from '@nestjs/mapped-types'; +import { RelationalGoalKpiTemplateDTO } from '../../goal-kpi-template/dto'; +import { RelationalGoalTemplateDTO } from '../../goal-template/dto'; +import { KeyresultTemplateDTO } from './keyresult-template.dto'; -export class CreateKeyresultTemplateDTO extends IntersectionType ( - KeyresultTemplateDTO, RelationalGoalTemplateDTO, RelationalGoalKpiTemplateDTO) {} \ No newline at end of file +export class CreateKeyresultTemplateDTO extends IntersectionType( + KeyresultTemplateDTO, + RelationalGoalTemplateDTO, + RelationalGoalKpiTemplateDTO +) {} diff --git a/packages/core/src/keyresult-update/dto/keyresult-update.dto.ts b/packages/core/src/keyresult-update/dto/keyresult-update.dto.ts index 01fbba1e5dd..bc55eb2591e 100644 --- a/packages/core/src/keyresult-update/dto/keyresult-update.dto.ts +++ b/packages/core/src/keyresult-update/dto/keyresult-update.dto.ts @@ -1,36 +1,35 @@ -import { IKeyResult } from "@gauzy/contracts"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from "class-validator"; -import { TenantOrganizationBaseDTO } from "core/dto"; +import { IKeyResult } from '@gauzy/contracts'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from 'class-validator'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; export class KeyresultUpdateDTO extends TenantOrganizationBaseDTO { + @ApiProperty({ type: () => String, readOnly: true }) + @IsNotEmpty() + @IsString() + readonly owner: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsNotEmpty() - @IsString() - readonly owner: string; + @ApiProperty({ type: () => Number, readOnly: true }) + @IsNotEmpty() + @IsNumber() + readonly progress: number; - @ApiProperty({ type: () => Number, readOnly: true }) - @IsNotEmpty() - @IsNumber() - readonly progress: number; + @ApiProperty({ type: () => Number, readOnly: true }) + @IsNotEmpty() + @IsNumber() + readonly update: number; - @ApiProperty({ type: () => Number, readOnly: true }) - @IsNotEmpty() - @IsNumber() - readonly update: number; + @ApiProperty({ type: () => String, readOnly: true }) + @IsNotEmpty() + @IsString() + readonly status: string; - @ApiProperty({ type: () => String, readOnly: true }) - @IsNotEmpty() - @IsString() - readonly status: string; + @ApiPropertyOptional({ type: () => String, readOnly: true }) + @IsOptional() + readonly keyResultId: string; - @ApiPropertyOptional({ type: () => String, readOnly: true }) - @IsOptional() - readonly keyResultId: string; - - @ApiProperty({ type: () => Object, readOnly: true }) - @IsOptional() - @IsObject() - readonly keyResult: IKeyResult; -} \ No newline at end of file + @ApiProperty({ type: () => Object, readOnly: true }) + @IsOptional() + @IsObject() + readonly keyResult: IKeyResult; +} diff --git a/packages/core/src/keyresult/dto/keyresult.dto.ts b/packages/core/src/keyresult/dto/keyresult.dto.ts index 18aba61ce52..21891dcdcaa 100644 --- a/packages/core/src/keyresult/dto/keyresult.dto.ts +++ b/packages/core/src/keyresult/dto/keyresult.dto.ts @@ -1,130 +1,128 @@ -import { IEmployee, IGoal, IKPI, IOrganizationProject, ITask } from "@gauzy/contracts"; -import { ApiProperty } from "@nestjs/swagger"; -import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"; -import { KeyResultUpdate } from "core"; +import { IEmployee, IGoal, IKPI, IOrganizationProject, ITask } from '@gauzy/contracts'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsObject, IsOptional, IsString } from 'class-validator'; +import { KeyResultUpdate } from '../../core/entities/internal'; export abstract class KeyresultDTO { - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly name: string; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly description: string; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly type: string; - - @ApiProperty({ type: () => Number, readOnly: true }) - @IsOptional() - @IsNumber() - readonly targetValue: number; - - @ApiProperty({ type: () => Number, readOnly: true }) - @IsOptional() - @IsNumber() - readonly initialValue: number; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly unit: string; - - @ApiProperty({ type: () => Number, readOnly: true }) - @IsOptional() - @IsNumber() - readonly update: number; - - @ApiProperty({ type: () => Number, readOnly: true }) - @IsOptional() - @IsNumber() - readonly progress: number; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly deadline: string; - - @ApiProperty({ type: () => Date, readOnly: true }) - @IsOptional() - readonly hardDeadline: Date; - - @ApiProperty({ type: () => Date, readOnly: true }) - @IsOptional() - readonly softDeadline: Date; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly status: string; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly weight: string; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly ownerId: string; - - @ApiProperty({ type: () => Object, readOnly: true }) - @IsOptional() - @IsObject() - readonly owner: IEmployee; - - @ApiProperty({ type: () => Object, readOnly: true }) - @IsOptional() - @IsObject() - readonly lead: IEmployee; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly leadId: string; - - @ApiProperty({ type: () => Object, readOnly: true }) - @IsOptional() - @IsObject() - readonly project: IOrganizationProject; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly projectId: string; - - @ApiProperty({ type: () => Object, readOnly: true }) - @IsOptional() - @IsObject() - readonly task: ITask; - - @ApiProperty({ type: () => Object, readOnly: true }) - @IsOptional() - @IsObject() - readonly kpi: IKPI; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly kpiId: string; - - @ApiProperty({ type: () => Object, readOnly: true }) - @IsOptional() - @IsObject() - readonly goal: IGoal; - - @ApiProperty({ type: () => String, readOnly: true }) - @IsOptional() - @IsString() - readonly goalId: string; - - @ApiProperty({ type: () => Object, isArray: true , readOnly: true }) - @IsOptional() - readonly updates: KeyResultUpdate[]; - -} \ No newline at end of file + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly name: string; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly description: string; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly type: string; + + @ApiProperty({ type: () => Number, readOnly: true }) + @IsOptional() + @IsNumber() + readonly targetValue: number; + + @ApiProperty({ type: () => Number, readOnly: true }) + @IsOptional() + @IsNumber() + readonly initialValue: number; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly unit: string; + + @ApiProperty({ type: () => Number, readOnly: true }) + @IsOptional() + @IsNumber() + readonly update: number; + + @ApiProperty({ type: () => Number, readOnly: true }) + @IsOptional() + @IsNumber() + readonly progress: number; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly deadline: string; + + @ApiProperty({ type: () => Date, readOnly: true }) + @IsOptional() + readonly hardDeadline: Date; + + @ApiProperty({ type: () => Date, readOnly: true }) + @IsOptional() + readonly softDeadline: Date; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly status: string; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly weight: string; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly ownerId: string; + + @ApiProperty({ type: () => Object, readOnly: true }) + @IsOptional() + @IsObject() + readonly owner: IEmployee; + + @ApiProperty({ type: () => Object, readOnly: true }) + @IsOptional() + @IsObject() + readonly lead: IEmployee; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly leadId: string; + + @ApiProperty({ type: () => Object, readOnly: true }) + @IsOptional() + @IsObject() + readonly project: IOrganizationProject; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly projectId: string; + + @ApiProperty({ type: () => Object, readOnly: true }) + @IsOptional() + @IsObject() + readonly task: ITask; + + @ApiProperty({ type: () => Object, readOnly: true }) + @IsOptional() + @IsObject() + readonly kpi: IKPI; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly kpiId: string; + + @ApiProperty({ type: () => Object, readOnly: true }) + @IsOptional() + @IsObject() + readonly goal: IGoal; + + @ApiProperty({ type: () => String, readOnly: true }) + @IsOptional() + @IsString() + readonly goalId: string; + + @ApiProperty({ type: () => Object, isArray: true, readOnly: true }) + @IsOptional() + readonly updates: KeyResultUpdate[]; +} diff --git a/packages/core/src/organization-contact/commands/handlers/organization-contact-create.handler.ts b/packages/core/src/organization-contact/commands/handlers/organization-contact-create.handler.ts index 638310502de..1552159ba9a 100644 --- a/packages/core/src/organization-contact/commands/handlers/organization-contact-create.handler.ts +++ b/packages/core/src/organization-contact/commands/handlers/organization-contact-create.handler.ts @@ -1,21 +1,20 @@ -import { IOrganizationContact, IOrganizationProject } from '@gauzy/contracts'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { isEmpty, isNotEmpty } from '@gauzy/common'; import { In } from 'typeorm'; +import { IOrganizationContact, IOrganizationProject } from '@gauzy/contracts'; +import { isEmpty, isNotEmpty } from '@gauzy/common'; import { OrganizationContactCreateCommand } from '../organization-contact-create.command'; import { OrganizationContactService } from '../../organization-contact.service'; -import { OrganizationProjectService } from './../../../organization-project/organization-project.service'; -import { RequestContext } from './../../../core/context'; -import { ContactService } from 'contact/contact.service'; +import { OrganizationProjectService } from '../../../organization-project/organization-project.service'; +import { RequestContext } from '../../../core/context'; +import { ContactService } from '../../../contact/contact.service'; @CommandHandler(OrganizationContactCreateCommand) export class OrganizationContactCreateHandler implements ICommandHandler { - constructor( private readonly _organizationContactService: OrganizationContactService, private readonly _organizationProjectService: OrganizationProjectService, - private readonly _contactService: ContactService, - ) { } + private readonly _contactService: ContactService + ) {} /** * Executes the creation of an organization contact. @@ -57,7 +56,10 @@ export class OrganizationContactCreateHandler implements ICommandHandler { +export class OrganizationProjectSettingUpdateHandler + implements ICommandHandler +{ private readonly logger = new Logger('OrganizationProjectSettingUpdateHandler'); - constructor( - private readonly _organizationProjectService: OrganizationProjectService - ) { } + constructor(private readonly _organizationProjectService: OrganizationProjectService) {} /** * Execute an organization project setting update command. @@ -18,9 +18,7 @@ export class OrganizationProjectSettingUpdateHandler implements ICommandHandler< * @param command - An `OrganizationProjectSettingUpdateCommand` object containing the update details. * @returns A promise that resolves to an `IOrganizationProjectSetting` or an `UpdateResult` object representing the result of the update operation. */ - public async execute( - command: OrganizationProjectSettingUpdateCommand - ): Promise { + public async execute(command: OrganizationProjectSettingUpdateCommand): Promise { try { // Extract the 'id' and 'input' properties from the command object. const { id, input } = command; @@ -33,8 +31,10 @@ export class OrganizationProjectSettingUpdateHandler implements ICommandHandler< } catch (error) { // Handle errors and return an appropriate error response this.logger.error('Failed to update project integration settings', error.message); - throw new HttpException(`Failed to update project integration settings: ${error.message}`, HttpStatus.BAD_REQUEST); + throw new HttpException( + `Failed to update project integration settings: ${error.message}`, + HttpStatus.BAD_REQUEST + ); } } - } diff --git a/packages/core/src/organization-project/commands/organization-project-setting.update.command.ts b/packages/core/src/organization-project/commands/organization-project-setting.update.command.ts index f2f9b8167e1..d7d70cad798 100644 --- a/packages/core/src/organization-project/commands/organization-project-setting.update.command.ts +++ b/packages/core/src/organization-project/commands/organization-project-setting.update.command.ts @@ -1,11 +1,8 @@ import { ICommand } from '@nestjs/cqrs'; -import { IOrganizationProject, IOrganizationProjectSetting } from '@gauzy/contracts'; +import { ID, IOrganizationProjectSetting } from '@gauzy/contracts'; export class OrganizationProjectSettingUpdateCommand implements ICommand { static readonly type = '[Organization Project Setting] Update'; - constructor( - public readonly id: IOrganizationProject['id'], - public readonly input: IOrganizationProjectSetting - ) { } + constructor(public readonly id: ID, public readonly input: IOrganizationProjectSetting) {} } diff --git a/packages/core/src/organization-project/dto/update-project-setting.dto.ts b/packages/core/src/organization-project/dto/update-project-setting.dto.ts index 7ae55390cca..a6608432756 100644 --- a/packages/core/src/organization-project/dto/update-project-setting.dto.ts +++ b/packages/core/src/organization-project/dto/update-project-setting.dto.ts @@ -1,31 +1,33 @@ -import { IOrganizationProjectSetting } from "@gauzy/contracts"; -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsBoolean, IsOptional, IsString, IsUUID } from "class-validator"; -import { TenantOrganizationBaseDTO } from "../../core/dto"; +import { IOrganizationProjectSetting } from '@gauzy/contracts'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsOptional, IsString } from 'class-validator'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; export class UpdateProjectSettingDTO extends TenantOrganizationBaseDTO implements IOrganizationProjectSetting { + /* + |-------------------------------------------------------------------------- + | Embeddable Columns + |-------------------------------------------------------------------------- + */ + @ApiPropertyOptional({ type: 'object' }) + @IsOptional() + readonly customFields?: Record; - // External repository ID property - @ApiPropertyOptional({ type: String }) - @IsOptional() - @IsUUID() - readonly repositoryId: string; + // Auto-sync tasks property + @ApiPropertyOptional({ type: Boolean }) + @IsOptional() + @IsBoolean() + readonly isTasksAutoSync: boolean; - // Auto-sync tasks property - @ApiPropertyOptional({ type: Boolean }) - @IsOptional() - @IsBoolean() - readonly isTasksAutoSync: boolean; + // Auto-sync on label property + @ApiPropertyOptional({ type: Boolean }) + @IsOptional() + @IsBoolean() + readonly isTasksAutoSyncOnLabel: boolean; - // Auto-sync on label property - @ApiPropertyOptional({ type: Boolean }) - @IsOptional() - @IsBoolean() - readonly isTasksAutoSyncOnLabel: boolean; - - // Auto-sync tasks label property - @ApiPropertyOptional({ type: String }) - @IsOptional() - @IsString() - readonly syncTag: string; + // Auto-sync tasks label property + @ApiPropertyOptional({ type: String }) + @IsOptional() + @IsString() + readonly syncTag: string; } diff --git a/packages/core/src/organization-project/organization-project.controller.ts b/packages/core/src/organization-project/organization-project.controller.ts index b7dea64d8b5..1433f7727e0 100644 --- a/packages/core/src/organization-project/organization-project.controller.ts +++ b/packages/core/src/organization-project/organization-project.controller.ts @@ -15,8 +15,8 @@ import { CommandBus } from '@nestjs/cqrs'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { DeleteResult } from 'typeorm'; import { + ID, IEditEntityByMemberInput, - IEmployee, IOrganizationProject, IOrganizationProjectSetting, IPagination, @@ -75,10 +75,10 @@ export class OrganizationProjectController extends CrudController { return await this.organizationProjectService.findByEmployee(employeeId, options); @@ -105,7 +105,7 @@ export class OrganizationProjectController extends CrudController { return await this.commandBus.execute(new OrganizationProjectEditByEmployeeCommand(body)); } @@ -135,7 +135,7 @@ export class OrganizationProjectController extends CrudController { return await this.commandBus.execute(new OrganizationProjectUpdateCommand({ ...entity, id })); @@ -151,7 +151,7 @@ export class OrganizationProjectController extends CrudController { return await this.commandBus.execute(new OrganizationProjectSettingUpdateCommand(id, entity)); @@ -188,7 +188,7 @@ export class OrganizationProjectController extends CrudController): Promise { @@ -214,7 +214,7 @@ export class OrganizationProjectController extends CrudController @@ -241,7 +241,7 @@ export class OrganizationProjectController extends CrudController): Promise> { return await this.organizationProjectService.findAll(params); @@ -254,9 +254,9 @@ export class OrganizationProjectController extends CrudController { return await this.organizationProjectService.findOneByIdString(id, options); @@ -269,9 +269,9 @@ export class OrganizationProjectController extends CrudController { return await this.commandBus.execute(new OrganizationProjectCreateCommand(entity)); } @@ -284,11 +284,11 @@ export class OrganizationProjectController extends CrudController { return await this.commandBus.execute(new OrganizationProjectUpdateCommand({ ...entity, id })); @@ -311,8 +311,8 @@ export class OrganizationProjectController extends CrudController { + @Delete('/:id') + async delete(@Param('id', UUIDValidationPipe) id: ID): Promise { return await this.organizationProjectService.delete(id); } } diff --git a/packages/core/src/organization-project/organization-project.entity.ts b/packages/core/src/organization-project/organization-project.entity.ts index ea90b241f01..5775113d7e0 100644 --- a/packages/core/src/organization-project/organization-project.entity.ts +++ b/packages/core/src/organization-project/organization-project.entity.ts @@ -1,19 +1,15 @@ -import { - JoinColumn, - RelationId, - JoinTable, -} from 'typeorm'; +import { JoinColumn, RelationId, JoinTable } from 'typeorm'; import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsOptional, IsString, IsUUID } from 'class-validator'; import { CurrenciesEnum, IActivity, + ID, IEmployee, IExpense, IImageAsset, IInvoiceItem, IOrganizationContact, - IOrganizationGithubRepository, IOrganizationProject, IOrganizationSprint, IOrganizationTeam, @@ -29,7 +25,7 @@ import { OrganizationProjectBudgetTypeEnum, ProjectBillingEnum, ProjectOwnerEnum, - TaskListTypeEnum, + TaskListTypeEnum } from '@gauzy/contracts'; import { isMySQL } from '@gauzy/config'; import { @@ -39,7 +35,6 @@ import { ImageAsset, InvoiceItem, OrganizationContact, - OrganizationGithubRepository, OrganizationSprint, OrganizationTeam, Payment, @@ -53,12 +48,29 @@ import { TenantOrganizationBaseEntity, TimeLog } from '../core/entities/internal'; -import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToMany, MultiORMManyToOne, MultiORMOneToMany } from './../core/decorators/entity'; +import { + ColumnIndex, + EmbeddedColumn, + MultiORMColumn, + MultiORMEntity, + MultiORMManyToMany, + MultiORMManyToOne, + MultiORMOneToMany +} from '../core/decorators/entity'; import { MikroOrmOrganizationProjectRepository } from './repository/mikro-orm-organization-project.repository'; +import { + MikroOrmOrganizationProjectEntityCustomFields, + OrganizationProjectEntityCustomFields, + TypeOrmOrganizationProjectEntityCustomFields +} from '../core/entities/custom-entity-fields/organization-project'; +import { HasCustomFields } from '../core/entities/custom-entity-fields'; +import { Taggable } from '../tags/tag.types'; @MultiORMEntity('organization_project', { mikroOrmRepository: () => MikroOrmOrganizationProjectRepository }) -export class OrganizationProject extends TenantOrganizationBaseEntity implements IOrganizationProject { - +export class OrganizationProject + extends TenantOrganizationBaseEntity + implements IOrganizationProject, Taggable, HasCustomFields +{ @ColumnIndex() @MultiORMColumn() name: string; @@ -115,10 +127,7 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements @MultiORMColumn({ nullable: true, default: OrganizationProjectBudgetTypeEnum.COST, - ...(isMySQL() ? - { type: 'enum', enum: OrganizationProjectBudgetTypeEnum } - : { type: 'text' } - ) + ...(isMySQL() ? { type: 'enum', enum: OrganizationProjectBudgetTypeEnum } : { type: 'text' }) }) budgetType?: OrganizationProjectBudgetTypeEnum; @@ -155,30 +164,6 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements |-------------------------------------------------------------------------- */ - /** - * OrganizationGithubRepository Relationship - */ - @MultiORMManyToOne(() => OrganizationGithubRepository, (it) => it.projects, { - /** Indicates if the relation column value can be nullable or not. */ - nullable: true, - - /** Defines the database cascade action on delete. */ - onDelete: 'SET NULL' - }) - @JoinColumn() - repository?: IOrganizationGithubRepository; - - /** - * Repository ID - */ - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsUUID() - @RelationId((it: OrganizationProject) => it.repository) - @ColumnIndex() - @MultiORMColumn({ nullable: true, relationId: true }) - repositoryId?: IOrganizationGithubRepository['id']; - /** * Organization Contact Relationship */ @@ -204,7 +189,7 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements @RelationId((it: OrganizationProject) => it.organizationContact) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) - organizationContactId?: IOrganizationContact['id']; + organizationContactId?: ID; /** * ImageAsset Relationship @@ -217,7 +202,7 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements onDelete: 'SET NULL', /** Eager relations are always loaded automatically when relation's owner entity is loaded using find* methods. */ - eager: true, + eager: true }) @JoinColumn() image?: IImageAsset; @@ -231,7 +216,7 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements @RelationId((it: OrganizationProject) => it.image) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) - imageId?: IImageAsset['id']; + imageId?: ID; /* |-------------------------------------------------------------------------- @@ -327,12 +312,11 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements onDelete: 'CASCADE', owner: true, pivotTable: 'tag_organization_project', - joinColumn: 'organizationProjectId', - inverseJoinColumn: 'tagId', + inverseJoinColumn: 'tagId' }) @JoinTable({ - name: 'tag_organization_project', + name: 'tag_organization_project' }) tags: ITag[]; @@ -343,7 +327,7 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements /** Defines the database action to perform on update. */ onUpdate: 'CASCADE', /** Defines the database cascade action on delete. */ - onDelete: 'CASCADE', + onDelete: 'CASCADE' }) members?: IEmployee[]; @@ -358,10 +342,21 @@ export class OrganizationProject extends TenantOrganizationBaseEntity implements owner: true, pivotTable: 'organization_project_team', joinColumn: 'organizationProjectId', - inverseJoinColumn: 'organizationTeamId', + inverseJoinColumn: 'organizationTeamId' }) @JoinTable({ name: 'organization_project_team' }) teams?: IOrganizationTeam[]; + + /* + |-------------------------------------------------------------------------- + | Embeddable Columns + |-------------------------------------------------------------------------- + */ + @EmbeddedColumn({ + mikroOrmEmbeddableEntity: () => MikroOrmOrganizationProjectEntityCustomFields, + typeOrmEmbeddableEntity: () => TypeOrmOrganizationProjectEntityCustomFields + }) + customFields?: OrganizationProjectEntityCustomFields; } diff --git a/packages/core/src/organization-project/organization-project.service.ts b/packages/core/src/organization-project/organization-project.service.ts index e374bec273e..e517279cf3e 100644 --- a/packages/core/src/organization-project/organization-project.service.ts +++ b/packages/core/src/organization-project/organization-project.service.ts @@ -1,19 +1,20 @@ import { Injectable } from '@nestjs/common'; import { Brackets, In, IsNull, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'; -import { isNotEmpty } from '@gauzy/common'; import { + ID, IEmployee, IOrganizationGithubRepository, IOrganizationProject, IOrganizationProjectsFindInput, IPagination } from '@gauzy/contracts'; -import { PaginationParams, TenantAwareCrudService } from './../core/crud'; +import { getConfig } from '@gauzy/config'; +import { CustomEmbeddedFieldConfig, isNotEmpty } from '@gauzy/common'; +import { PaginationParams, TenantAwareCrudService } from '../core/crud'; import { RequestContext } from '../core/context'; import { OrganizationProject } from './organization-project.entity'; import { prepareSQLQuery as p } from './../database/database.helper'; -import { TypeOrmOrganizationProjectRepository } from './repository/type-orm-organization-project.repository'; -import { MikroOrmOrganizationProjectRepository } from './repository/mikro-orm-organization-project.repository'; +import { MikroOrmOrganizationProjectRepository, TypeOrmOrganizationProjectRepository } from './repository'; @Injectable() export class OrganizationProjectService extends TenantAwareCrudService { @@ -60,15 +61,13 @@ export class OrganizationProjectService extends TenantAwareCrudService { try { const tenantId = RequestContext.currentTenantId() || options.tenantId; @@ -137,14 +131,16 @@ export class OrganizationProjectService extends TenantAwareCrudService( + query: SelectQueryBuilder, + customFields: CustomEmbeddedFieldConfig[] + ): SelectQueryBuilder { + const hasRepositoryField = customFields.some((field: CustomEmbeddedFieldConfig) => field.name === 'repository'); + + if (hasRepositoryField) { + // Join with the `Repository` entity and left join with `Issue` entity + query.innerJoinAndSelect(`${query.alias}.customFields.repository`, 'repository'); + query.leftJoin('repository.issues', 'issue'); + + // Select and count issues, and group the result by project and repository + query.addSelect('COUNT(issue.id)', 'issueCount'); + query.groupBy(`${query.alias}.id, repository.id`); + } + + return query; + } + + /** + * Adds custom where conditions based on provided options and tenant ID. + * + * @param query - The TypeORM query builder instance. + * @param tenantId - The tenant ID to be used in the where conditions. + * @param options - Additional options containing where conditions. + * @returns The modified query builder instance. + */ + addWhereConditions( + query: SelectQueryBuilder, + options?: { where?: Record } + ): SelectQueryBuilder { + const tenantId = RequestContext.currentTenantId(); + + // Define where conditions for the query + query.where((qb: SelectQueryBuilder) => { + qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId`), { tenantId }); + + // Conditionally add repository tenantId condition only if repository is joined + if (query.expressionMap.joinAttributes.some((ja) => ja.alias.name === 'repository')) { + qb.andWhere(p(`"repository"."tenantId" = :tenantId`), { tenantId }); + } + + if (options?.where) { + for (const key of Object.keys(options.where)) { + qb.andWhere(p(`"${qb.alias}"."${key}" = :${key}`), { [key]: options.where[key] }); + + // Conditionally add where conditions for repository if it's joined + if (query.expressionMap.joinAttributes.some((ja) => ja.alias.name === 'repository')) { + qb.andWhere(p(`"repository"."${key}" = :${key}`), { [key]: options.where[key] }); + } + } + } + + qb.andWhere(p(`"${qb.alias}"."repositoryId" IS NOT NULL`)); + }); + + return query; + } + /** * Find synchronized organization projects with options and count their associated issues. * @@ -165,6 +227,9 @@ export class OrganizationProjectService extends TenantAwareCrudService ): Promise> { + // Get the list of custom fields for the specified entity, defaulting to an empty array if none are found + const customFields = getConfig().customFields?.['OrganizationProject'] ?? []; + // Create a query builder for the `OrganizationProject` entity const query = this.typeOrmRepository.createQueryBuilder(this.tableName); @@ -172,35 +237,11 @@ export class OrganizationProjectService extends TenantAwareCrudService) => { - const tenantId = RequestContext.currentTenantId(); - qb.andWhere(p(`"${qb.alias}"."tenantId" = :tenantId`), { - tenantId - }); - qb.andWhere(p(`"repository"."tenantId" = :tenantId`), { - tenantId - }); - - if (options?.where) { - for (const key of Object.keys(options.where)) { - qb.andWhere(p(`"${query.alias}"."${key}" = :${key}`), { [key]: options.where[key] }); - } - for (const key of Object.keys(options.where)) { - qb.andWhere(p(`"repository"."${key}" = :${key}`), { [key]: options.where[key] }); - } - } - - qb.andWhere(p(`"${query.alias}"."repositoryId" IS NOT NULL`)); - }); + // Add where conditions + this.addWhereConditions(query, options); // Log the SQL query (for debugging) // console.log(await query.getRawMany()); diff --git a/packages/core/src/organization-project/repository/index.ts b/packages/core/src/organization-project/repository/index.ts new file mode 100644 index 00000000000..b6fa20a5750 --- /dev/null +++ b/packages/core/src/organization-project/repository/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-organization-project.repository'; +export * from './type-orm-organization-project.repository'; diff --git a/packages/core/src/organization-task-setting/organization-task-setting.controller.ts b/packages/core/src/organization-task-setting/organization-task-setting.controller.ts index c1dc19aac6a..3775e37c0bd 100644 --- a/packages/core/src/organization-task-setting/organization-task-setting.controller.ts +++ b/packages/core/src/organization-task-setting/organization-task-setting.controller.ts @@ -14,14 +14,14 @@ import { import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { CommandBus } from '@nestjs/cqrs'; import { IOrganizationTaskSetting, PermissionsEnum } from '@gauzy/contracts'; -import { Permissions } from './../shared/decorators'; -import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; -import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; +import { TenantOrganizationBaseDTO } from '../core/dto'; +import { Permissions } from '../shared/decorators'; +import { UUIDValidationPipe, UseValidationPipe } from '../shared/pipes'; +import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; import { OrganizationTaskSettingCreateCommand, OrganizationTaskSettingUpdateCommand } from './commands'; import { CreateOrganizationTaskSettingDTO, UpdateOrganizationTaskSettingDTO } from './dto'; import { OrganizationTaskSetting } from './organization-task-setting.entity'; import { OrganizationTaskSettingService } from './organization-task-setting.service'; -import { TenantOrganizationBaseDTO } from 'core/dto'; @ApiTags('OrganizationTaskSetting') @UseGuards(TenantPermissionGuard, PermissionGuard) @@ -31,7 +31,7 @@ export class OrganizationTaskSettingController { constructor( private readonly commandBus: CommandBus, private readonly organizationTaskSettingService: OrganizationTaskSettingService - ) { } + ) {} /** * GET organization Task Setting by organizationId diff --git a/packages/core/src/organization-team-join-request/organization-team-join-request.controller.ts b/packages/core/src/organization-team-join-request/organization-team-join-request.controller.ts index e0c6713a46b..85517281665 100644 --- a/packages/core/src/organization-team-join-request/organization-team-join-request.controller.ts +++ b/packages/core/src/organization-team-join-request/organization-team-join-request.controller.ts @@ -1,15 +1,4 @@ -import { - Body, - Controller, - Get, - HttpCode, - HttpStatus, - Param, - Post, - Put, - Query, - UseGuards -} from '@nestjs/common'; +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, UseGuards } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { CommandBus } from '@nestjs/cqrs'; import { I18nLang } from 'nestjs-i18n'; @@ -21,7 +10,7 @@ import { PermissionsEnum } from '@gauzy/contracts'; import { Public } from '@gauzy/common'; -import { PaginationParams } from 'core/crud'; +import { PaginationParams } from '../core/crud'; import { UUIDValidationPipe, UseValidationPipe } from '../shared/pipes'; import { LanguageDecorator, Permissions } from '../shared/decorators'; import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; @@ -36,7 +25,7 @@ export class OrganizationTeamJoinRequestController { constructor( private readonly _commandBus: CommandBus, private readonly _organizationTeamJoinRequestService: OrganizationTeamJoinRequestService - ) { } + ) {} /** * Validate organization team join request diff --git a/packages/core/src/organization-team-join-request/organization-team-join-request.service.ts b/packages/core/src/organization-team-join-request/organization-team-join-request.service.ts index 0fa41bc1ebb..a8e6290112c 100644 --- a/packages/core/src/organization-team-join-request/organization-team-join-request.service.ts +++ b/packages/core/src/organization-team-join-request/organization-team-join-request.service.ts @@ -30,8 +30,8 @@ import { TypeOrmOrganizationTeamJoinRequestRepository } from './repository/type- import { MikroOrmOrganizationTeamJoinRequestRepository } from './repository/mikro-orm-organization-team-join-request.repository'; import { TypeOrmUserRepository } from '../user/repository/type-orm-user.repository'; import { MikroOrmUserRepository } from '../user/repository/mikro-orm-user.repository'; -import { TypeOrmOrganizationTeamEmployeeRepository } from 'organization-team-employee/repository/type-orm-organization-team-employee.repository'; -import { MikroOrmOrganizationTeamEmployeeRepository } from 'organization-team-employee/repository/mikro-orm-organization-team-employee.repository'; +import { TypeOrmOrganizationTeamEmployeeRepository } from '../organization-team-employee/repository/type-orm-organization-team-employee.repository'; +import { MikroOrmOrganizationTeamEmployeeRepository } from '../organization-team-employee/repository/mikro-orm-organization-team-employee.repository'; @Injectable() export class OrganizationTeamJoinRequestService extends TenantAwareCrudService { diff --git a/packages/core/src/organization/organization.entity.ts b/packages/core/src/organization/organization.entity.ts index c3d87049e57..f6e0a07195b 100644 --- a/packages/core/src/organization/organization.entity.ts +++ b/packages/core/src/organization/organization.entity.ts @@ -20,7 +20,8 @@ import { IFeatureOrganization, IAccountingTemplate, IReportOrganization, - IImageAsset + IImageAsset, + ID } from '@gauzy/contracts'; import { AccountingTemplate, @@ -52,7 +53,6 @@ import { MikroOrmOrganizationRepository } from './repository/mikro-orm-organizat @MultiORMEntity('organization', { mikroOrmRepository: () => MikroOrmOrganizationRepository }) export class Organization extends TenantBaseEntity implements IOrganization { - @ColumnIndex() @MultiORMColumn() name: string; @@ -346,7 +346,7 @@ export class Organization extends TenantBaseEntity implements IOrganization { @RelationId((it: Organization) => it.contact) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) - contactId?: IContact['id']; + contactId?: ID; /** * ImageAsset @@ -362,7 +362,7 @@ export class Organization extends TenantBaseEntity implements IOrganization { eager: true }) @JoinColumn() - image?: ImageAsset; + image?: IImageAsset; @ApiPropertyOptional({ type: () => String }) @IsOptional() @@ -370,7 +370,7 @@ export class Organization extends TenantBaseEntity implements IOrganization { @RelationId((it: Organization) => it.image) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) - imageId?: IImageAsset['id']; + imageId?: ID; /* |-------------------------------------------------------------------------- @@ -439,7 +439,7 @@ export class Organization extends TenantBaseEntity implements IOrganization { owner: true, pivotTable: 'tag_organization', joinColumn: 'organizationId', - inverseJoinColumn: 'tagId', + inverseJoinColumn: 'tagId' }) @JoinTable({ name: 'tag_organization' diff --git a/packages/core/src/payment/dto/query/payment-report-query.dto.ts b/packages/core/src/payment/dto/query/payment-report-query.dto.ts index d9eff935984..e35cce480b2 100644 --- a/packages/core/src/payment/dto/query/payment-report-query.dto.ts +++ b/packages/core/src/payment/dto/query/payment-report-query.dto.ts @@ -1,7 +1,7 @@ import { IGetPaymentInput } from '@gauzy/contracts'; import { IntersectionType, PickType } from '@nestjs/swagger'; import { RelationsQueryDTO, SelectorsQueryDTO } from './../../../shared/dto'; -import { TimeLogQueryDTO } from 'time-tracking/time-log/dto/query'; +import { TimeLogQueryDTO } from '../../../time-tracking/time-log/dto/query'; /** * Get payment report request DTO validation diff --git a/packages/core/src/pipeline-stage/pipeline-stage.module.ts b/packages/core/src/pipeline-stage/pipeline-stage.module.ts index a3182a56668..f1a121f2305 100644 --- a/packages/core/src/pipeline-stage/pipeline-stage.module.ts +++ b/packages/core/src/pipeline-stage/pipeline-stage.module.ts @@ -3,13 +3,11 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { PipelineStage } from './pipeline-stage.entity'; import { StageService } from './pipeline-stage.service'; +import { TypeOrmPipelineStageRepository } from './repository/type-orm-pipeline-stage.repository'; @Module({ - imports: [ - TypeOrmModule.forFeature([PipelineStage]), - MikroOrmModule.forFeature([PipelineStage]) - ], - providers: [StageService], - exports: [StageService] + imports: [TypeOrmModule.forFeature([PipelineStage]), MikroOrmModule.forFeature([PipelineStage])], + providers: [StageService, TypeOrmPipelineStageRepository], + exports: [TypeOrmModule, MikroOrmModule, StageService, TypeOrmPipelineStageRepository] }) -export class StageModule { } +export class StageModule {} diff --git a/packages/core/src/pipeline-stage/pipeline-stage.service.ts b/packages/core/src/pipeline-stage/pipeline-stage.service.ts index b7c912d5538..11f2babf94f 100644 --- a/packages/core/src/pipeline-stage/pipeline-stage.service.ts +++ b/packages/core/src/pipeline-stage/pipeline-stage.service.ts @@ -1,4 +1,3 @@ -import { InjectRepository } from '@nestjs/typeorm'; import { Injectable } from '@nestjs/common'; import { TenantAwareCrudService } from './../core/crud'; import { PipelineStage } from './pipeline-stage.entity'; @@ -7,11 +6,8 @@ import { MikroOrmPipelineStageRepository } from './repository/mikro-orm-pipeline @Injectable() export class StageService extends TenantAwareCrudService { - constructor( - @InjectRepository(PipelineStage) typeOrmPipelineStageRepository: TypeOrmPipelineStageRepository, - mikroOrmPipelineStageRepository: MikroOrmPipelineStageRepository ) { super(typeOrmPipelineStageRepository, mikroOrmPipelineStageRepository); diff --git a/packages/core/src/pipeline/dto/create-pipeline.dto.ts b/packages/core/src/pipeline/dto/create-pipeline.dto.ts new file mode 100644 index 00000000000..d8830100bfb --- /dev/null +++ b/packages/core/src/pipeline/dto/create-pipeline.dto.ts @@ -0,0 +1,17 @@ +import { IPipelineCreateInput } from '@gauzy/contracts'; +import { IntersectionType, PickType } from '@nestjs/mapped-types'; +import { Pipeline } from '../../core/entities/internal'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; + +/** + * Pipeline DTO + */ +export class PipelineDTO extends IntersectionType( + TenantOrganizationBaseDTO, + PickType(Pipeline, ['name', 'description', 'stages', 'isActive', 'isArchived']) +) {} + +/** + * Create pipeline DTO + */ +export class CreatePipelineDTO extends PipelineDTO implements IPipelineCreateInput {} diff --git a/packages/core/src/pipeline/dto/index.ts b/packages/core/src/pipeline/dto/index.ts new file mode 100644 index 00000000000..e2dbcf1fbaf --- /dev/null +++ b/packages/core/src/pipeline/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-pipeline.dto'; +export * from './update-pipeline.dto'; diff --git a/packages/core/src/pipeline/dto/update-pipeline.dto.ts b/packages/core/src/pipeline/dto/update-pipeline.dto.ts new file mode 100644 index 00000000000..b6bd4fbbd1f --- /dev/null +++ b/packages/core/src/pipeline/dto/update-pipeline.dto.ts @@ -0,0 +1,6 @@ +import { CreatePipelineDTO } from './create-pipeline.dto'; + +/** + * Update pipeline DTO + */ +export class UpdatePipelineDTO extends CreatePipelineDTO {} diff --git a/packages/core/src/pipeline/pipeline.controller.ts b/packages/core/src/pipeline/pipeline.controller.ts index 7f89645537a..7330746ab94 100644 --- a/packages/core/src/pipeline/pipeline.controller.ts +++ b/packages/core/src/pipeline/pipeline.controller.ts @@ -12,15 +12,15 @@ import { UseGuards } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { DeepPartial } from 'typeorm'; -import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import { IDeal, IPagination, IPipeline, PermissionsEnum } from '@gauzy/contracts'; -import { CrudController, PaginationParams } from './../core/crud'; +import { DeleteResult, UpdateResult } from 'typeorm'; +import { ID, IDeal, IPagination, IPipeline, PermissionsEnum } from '@gauzy/contracts'; +import { CrudController, OptionParams, PaginationParams } from './../core/crud'; import { Pipeline } from './pipeline.entity'; import { PipelineService } from './pipeline.service'; import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; import { Permissions } from './../shared/decorators'; import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; +import { CreatePipelineDTO, UpdatePipelineDTO } from './dto'; @ApiTags('Pipeline') @UseGuards(TenantPermissionGuard, PermissionGuard) @@ -38,7 +38,7 @@ export class PipelineController extends CrudController { * @returns The paginated result of sales pipelines. */ @Permissions(PermissionsEnum.VIEW_SALES_PIPELINES) - @Get('pagination') + @Get('/pagination') @UseValidationPipe({ transform: true }) async pagination(@Query() filter: PaginationParams): Promise> { return await this.pipelineService.pagination(filter); @@ -56,26 +56,47 @@ export class PipelineController extends CrudController { description: 'Found records' }) @Permissions(PermissionsEnum.VIEW_SALES_PIPELINES) - @Get() + @Get('/') public async findAll(@Query() filter: PaginationParams): Promise> { return await this.pipelineService.findAll(filter); } /** - * Find deals for a specific sales pipeline with permissions, API documentation, and parameter validation. + * Get deals associated with a specific pipeline * - * @param id - The identifier of the sales pipeline. - * @returns A paginated result of deals for the specified sales pipeline. + * @param pipelineId The ID of the pipeline + * @param options Filter conditions for fetching the deals + * @returns A promise of paginated deals */ - @ApiOperation({ summary: 'find deals' }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'Found records' - }) + @ApiOperation({ summary: 'Get deals for a specific pipeline' }) + @ApiResponse({ status: 200, description: 'Success' }) + @ApiResponse({ status: 400, description: 'Bad Request' }) + @ApiResponse({ status: 404, description: 'Not Found' }) + @Permissions(PermissionsEnum.VIEW_SALES_PIPELINES) + @Get('/:pipelineId/deals') + async getPipelineDeals( + @Param('pipelineId', UUIDValidationPipe) pipelineId: ID, + @Query() options: OptionParams + ): Promise> { + return await this.pipelineService.getPipelineDeals(pipelineId, options.where); + } + + /** + * Find a Pipeline by ID + * + * @param id - The ID of the Pipeline to find + * @returns The found Pipeline + */ + @ApiOperation({ summary: 'Find a Pipeline by ID' }) + @ApiResponse({ status: 200, description: 'The found Pipeline' }) + @ApiResponse({ status: 404, description: 'Pipeline not found' }) @Permissions(PermissionsEnum.VIEW_SALES_PIPELINES) - @Get(':id/deals') - public async findDeals(@Param('id', UUIDValidationPipe) id: string): Promise> { - return await this.pipelineService.findDeals(id); + @Get('/:id') + async findById( + @Param('id', UUIDValidationPipe) id: ID, + @Query() options: OptionParams + ): Promise { + return await this.pipelineService.findById(id, options); } /** @@ -95,8 +116,9 @@ export class PipelineController extends CrudController { }) @HttpCode(HttpStatus.CREATED) @Permissions(PermissionsEnum.EDIT_SALES_PIPELINES) - @Post() - async create(@Body() entity: DeepPartial): Promise { + @Post('/') + @UseValidationPipe() + async create(@Body() entity: CreatePipelineDTO): Promise { return await this.pipelineService.create(entity); } @@ -123,11 +145,12 @@ export class PipelineController extends CrudController { }) @HttpCode(HttpStatus.ACCEPTED) @Permissions(PermissionsEnum.EDIT_SALES_PIPELINES) - @Put(':id') + @Put('/:id') + @UseValidationPipe() async update( - @Param('id', UUIDValidationPipe) id: string, - @Body() entity: QueryDeepPartialEntity - ): Promise { + @Param('id', UUIDValidationPipe) id: ID, + @Body() entity: UpdatePipelineDTO + ): Promise { return await this.pipelineService.update(id, entity); } @@ -149,8 +172,8 @@ export class PipelineController extends CrudController { }) @HttpCode(HttpStatus.ACCEPTED) @Permissions(PermissionsEnum.EDIT_SALES_PIPELINES) - @Delete(':id') - async delete(@Param('id', UUIDValidationPipe) id: string): Promise { + @Delete('/:id') + async delete(@Param('id', UUIDValidationPipe) id: ID): Promise { return await this.pipelineService.delete(id); } } diff --git a/packages/core/src/pipeline/pipeline.entity.ts b/packages/core/src/pipeline/pipeline.entity.ts index 0422e1dec17..252acfd114e 100644 --- a/packages/core/src/pipeline/pipeline.entity.ts +++ b/packages/core/src/pipeline/pipeline.entity.ts @@ -1,16 +1,12 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IPipeline, IPipelineStage } from '@gauzy/contracts'; -import { - PipelineStage, - TenantOrganizationBaseEntity -} from '../core/entities/internal'; +import { PipelineStage, TenantOrganizationBaseEntity } from '../core/entities/internal'; import { MultiORMColumn, MultiORMEntity, MultiORMOneToMany } from './../core/decorators/entity'; import { MikroOrmPipelineRepository } from './repository/mikro-orm-pipeline.repository'; @MultiORMEntity('pipeline', { mikroOrmRepository: () => MikroOrmPipelineRepository }) export class Pipeline extends TenantOrganizationBaseEntity implements IPipeline { - @ApiPropertyOptional({ type: () => String }) @IsOptional() @IsString() @@ -28,7 +24,6 @@ export class Pipeline extends TenantOrganizationBaseEntity implements IPipeline | @OneToMany |-------------------------------------------------------------------------- */ - @ApiProperty({ type: () => PipelineStage }) @MultiORMOneToMany(() => PipelineStage, (it) => it.pipeline, { cascade: ['insert'] }) @@ -39,14 +34,17 @@ export class Pipeline extends TenantOrganizationBaseEntity implements IPipeline | EventSubscriber |-------------------------------------------------------------------------- */ - + /** + * @BeforeInsert + */ public __before_persist?(): void { const pipelineId = this.id ? { pipelineId: this.id } : {}; let index = 0; - this.stages?.forEach((stage) => { - Object.assign(stage, pipelineId, { index: ++index }); - }); - console.log(this.stages); + if (this.stages) { + this.stages.forEach((stage) => { + Object.assign(stage, pipelineId, { index: ++index }); + }); + } } } diff --git a/packages/core/src/pipeline/pipeline.service.ts b/packages/core/src/pipeline/pipeline.service.ts index 26de7e67f62..551842d513b 100644 --- a/packages/core/src/pipeline/pipeline.service.ts +++ b/packages/core/src/pipeline/pipeline.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { DeepPartial, FindManyOptions, FindOptionsWhere, Raw, UpdateResult } from 'typeorm'; +import { FindManyOptions, FindOneOptions, FindOptionsWhere, Raw, UpdateResult } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; -import { IDeal, IPagination, IPipeline, IPipelineStage } from '@gauzy/contracts'; +import { ID, IDeal, IPagination, IPipeline, IPipelineStage } from '@gauzy/contracts'; import { isPostgres } from '@gauzy/config'; import { ConnectionEntityManager } from '../database/connection-entity-manager'; import { prepareSQLQuery as p } from './../database/database.helper'; @@ -16,22 +16,44 @@ import { MikroOrmPipelineRepository, TypeOrmPipelineRepository } from './reposit @Injectable() export class PipelineService extends TenantAwareCrudService { public constructor( - private readonly typeOrmPipelineRepository: TypeOrmPipelineRepository, - private readonly mikroOrmPipelineRepository: MikroOrmPipelineRepository, - private readonly typeOrmDealRepository: TypeOrmDealRepository, - private readonly typeOrmUserRepository: TypeOrmUserRepository, - private readonly _connectionEntityManager: ConnectionEntityManager + readonly typeOrmPipelineRepository: TypeOrmPipelineRepository, + readonly mikroOrmPipelineRepository: MikroOrmPipelineRepository, + readonly typeOrmDealRepository: TypeOrmDealRepository, + readonly typeOrmUserRepository: TypeOrmUserRepository, + readonly connectionEntityManager: ConnectionEntityManager ) { super(typeOrmPipelineRepository, mikroOrmPipelineRepository); } - public async findDeals(pipelineId: string) { + /** + * Find a Pipeline by ID + * + * @param id - The ID of the Pipeline to find + * @param relations - Optional relations to include in the query + * @returns The found Pipeline + */ + async findById(id: ID, options?: FindOneOptions): Promise { + return await super.findOneByIdString(id, options); + } + + /** + * Finds deals for a given pipeline. + * + * @param pipelineId - The ID of the pipeline to find deals for. + * @returns An object containing an array of deals and the total number of deals. + */ + public async getPipelineDeals(pipelineId: ID, where?: FindOptionsWhere) { + // Retrieve the current tenant ID from the request context const tenantId = RequestContext.currentTenantId(); + const { organizationId } = where || {}; + + // Fetch deals related to the pipeline, grouping by stage and deal IDs const items: IDeal[] = await this.typeOrmDealRepository .createQueryBuilder('deal') .leftJoin('deal.stage', 'pipeline_stage') .where(p('pipeline_stage.pipelineId = :pipelineId'), { pipelineId }) .andWhere(p('pipeline_stage.tenantId = :tenantId'), { tenantId }) + .andWhere(p('pipeline_stage.organizationId = :organizationId'), { organizationId }) .groupBy(p('pipeline_stage.id')) // FIX: error: column "deal.id" must appear in the GROUP BY clause or be used in an aggregate function .addGroupBy(p('deal.id')) @@ -39,69 +61,85 @@ export class PipelineService extends TenantAwareCrudService { .orderBy(p('pipeline_stage.index'), 'ASC') .getMany(); + // Get the total number of deals const { length: total } = items; + // For each deal, fetch the user who created it for (const deal of items) { - deal.createdBy = await this.typeOrmUserRepository.findOneBy({ - id: deal.createdByUserId - }); + deal.createdBy = await this.typeOrmUserRepository.findOneBy({ id: deal.createdByUserId }); } + // Return the deals and their total count return { items, total }; } /** + * Updates a Pipeline entity and its stages within a transaction. * - * @param id - * @param entity - * @returns + * @param id - The ID of the Pipeline to update. + * @param entity - The partial entity data to update. + * @returns The result of the update operation. */ - public async update( - id: string | number | FindOptionsWhere, - entity: QueryDeepPartialEntity - ): Promise { - const queryRunner = this._connectionEntityManager.rawConnection.createQueryRunner(); + public async update(id: ID, partialEntity: QueryDeepPartialEntity): Promise { + const queryRunner = this.connectionEntityManager.rawConnection.createQueryRunner(); try { - /** - * Query runner connect & start transaction - */ + // Retrieve the current tenant ID from the request context + const tenantId = RequestContext.currentTenantId(); + + // Connect and start transaction await queryRunner.connect(); await queryRunner.startTransaction(); - await queryRunner.manager.findOneByOrFail(Pipeline, { - id: id as any + // Fetch the existing pipeline + await queryRunner.manager.findOneByOrFail(Pipeline, { id, tenantId }); + + // Create a new pipeline instance with the updated data + const pipeline: Pipeline = queryRunner.manager.create( + Pipeline, + new Pipeline({ + ...partialEntity, + id, + tenantId + }) + ); + + // Fetch existing pipeline stages + const existingStages: IPipelineStage[] = await queryRunner.manager.findBy(PipelineStage, { + pipelineId: id, + tenantId }); - const pipeline: Pipeline = await queryRunner.manager.create(Pipeline, { id: id as any, ...entity } as any); + // Get the updated and existing stages const updatedStages: IPipelineStage[] = pipeline.stages?.filter((stage: IPipelineStage) => stage.id) || []; - const stages: IPipelineStage[] = await queryRunner.manager.findBy(PipelineStage, { - pipelineId: id as any - }); + // Create a list of stage IDs that are being updated + const requestStageIds = updatedStages.map((stage) => stage.id); + + // Identify stages to be deleted + const deletedStages = existingStages.filter((stage) => !requestStageIds.includes(stage.id)); - const requestStageIds = updatedStages.map((updatedStage: IPipelineStage) => updatedStage.id); - const deletedStages = stages.filter((stage: IPipelineStage) => !requestStageIds.includes(stage.id)); - const createdStages = pipeline.stages?.filter((stage: IPipelineStage) => !updatedStages.includes(stage)) || []; + //Identify stages to be created + const createdStages = (pipeline.stages ?? []).filter( + (stage) => !updatedStages.some((updatedStage) => updatedStage.id === stage.id) + ); + // Prepare the pipeline for saving pipeline.__before_persist(); delete pipeline.stages; - await queryRunner.manager.remove(deletedStages); + // Perform stage deletions, creations, and updates concurrently + await Promise.all([ + ...deletedStages.map((stage) => queryRunner.manager.remove(PipelineStage, stage)), + ...createdStages.map((stage) => queryRunner.manager.save(PipelineStage, stage)), + ...updatedStages.map((stage) => queryRunner.manager.save(PipelineStage, stage)) + ]); - for await (const stage of createdStages) { - await queryRunner.manager.save( - queryRunner.manager.create(PipelineStage, stage as DeepPartial) - ); - } - for await (const stage of updatedStages) { - await queryRunner.manager.update(PipelineStage, stage.id, stage); - } - - const saved = await queryRunner.manager.update(Pipeline, id, pipeline); + // Save the updated pipeline + const updatePipeline = await queryRunner.manager.save(Pipeline, pipeline); await queryRunner.commitTransaction(); - return saved; + return updatePipeline; } catch (error) { console.log('Rollback Pipeline Transaction', error); await queryRunner.rollbackTransaction(); @@ -117,20 +155,18 @@ export class PipelineService extends TenantAwareCrudService { * @returns The paginated result. */ public async pagination(filter: FindManyOptions): Promise> { - if ('where' in filter) { - const { where } = filter; + if (filter.where) { const likeOperator = isPostgres() ? 'ILIKE' : 'LIKE'; - if ('name' in where) { - const { name } = where; - filter['where']['name'] = Raw((alias) => `${alias} ${likeOperator} '%${name}%'`); + const { name, description, stages } = filter.where as any; // Type assertion for easier destructuring + + if (name) { + filter.where['name'] = Raw((alias) => `${alias} ${likeOperator} '%${name}%'`); } - if ('description' in where) { - const { description } = where; - filter['where']['description'] = Raw((alias) => `${alias} ${likeOperator} '%${description}%'`); + if (description) { + filter.where['description'] = Raw((alias) => `${alias} ${likeOperator} '%${description}%'`); } - if ('stages' in where) { - const { stages } = where; - filter['where']['stages'] = { + if (stages) { + filter.where['stages'] = { name: Raw((alias) => `${alias} ${likeOperator} '%${stages}%'`) }; } diff --git a/packages/core/src/pipeline/pipeline.subscriber.ts b/packages/core/src/pipeline/pipeline.subscriber.ts index a974b92a78d..62f717b6d35 100644 --- a/packages/core/src/pipeline/pipeline.subscriber.ts +++ b/packages/core/src/pipeline/pipeline.subscriber.ts @@ -1,76 +1,78 @@ -import { EventSubscriber } from "typeorm"; -import { Pipeline } from "./pipeline.entity"; -import { BaseEntityEventSubscriber } from "../core/entities/subscribers/base-entity-event.subscriber"; +import { EventSubscriber } from 'typeorm'; +import { Pipeline } from './pipeline.entity'; +import { BaseEntityEventSubscriber } from '../core/entities/subscribers/base-entity-event.subscriber'; @EventSubscriber() export class PipelineSubscriber extends BaseEntityEventSubscriber { + /** + * Indicates that this subscriber only listen to Pipeline events. + */ + listenTo() { + return Pipeline; + } - /** - * Indicates that this subscriber only listen to Pipeline events. - */ - listenTo() { - return Pipeline; - } + /** + * Called after a Pipeline entity is loaded from the database. This method performs + * additional operations defined in the __after_fetch method on the loaded entity. + * + * @param entity The Pipeline entity that has been loaded. + * @returns {Promise} A promise that resolves when the post-load processing is complete. + */ + async afterEntityLoad(entity: Pipeline): Promise { + try { + this.__after_fetch(entity); + } catch (error) { + console.error( + `PipelineSubscriber: An error occurred during the afterEntityLoad process for Pipeline ID ${entity.id}:`, + error + ); + } + } - /** - * Called after a Pipeline entity is loaded from the database. This method performs - * additional operations defined in the __after_fetch method on the loaded entity. - * - * @param entity The Pipeline entity that has been loaded. - * @returns {Promise} A promise that resolves when the post-load processing is complete. - */ - async afterEntityLoad(entity: Pipeline): Promise { - try { - this.__after_fetch(entity); - } catch (error) { - console.error(`PipelineSubscriber: An error occurred during the afterEntityLoad process for Pipeline ID ${entity.id}:`, error); - } - } + /** + * Called before a Pipeline entity is inserted or created in the database. This method + * assigns pipeline ID and an index to each stage in the pipeline. + * + * @param entity The Pipeline entity about to be created. + * @returns {Promise} A promise that resolves when the pre-creation processing is complete. + */ + async beforeEntityCreate(entity: Pipeline): Promise { + try { + // Assign pipeline ID to each stage and set an incrementing index + const pipelineId = entity?.id ? { pipelineId: entity.id } : {}; + let index = 0; - /** - * Called before a Pipeline entity is inserted or created in the database. This method - * assigns pipeline ID and an index to each stage in the pipeline. - * - * @param entity The Pipeline entity about to be created. - * @returns {Promise} A promise that resolves when the pre-creation processing is complete. - */ - async beforeEntityCreate(entity: Pipeline): Promise { - try { - // Assign pipeline ID to each stage and set an incrementing index - const pipelineId = entity?.id ? { pipelineId: entity.id } : {}; - let index = 0; + entity?.stages?.forEach((stage) => { + Object.assign(stage, pipelineId, { index: ++index }); + }); + } catch (error) { + console.error('PipelineSubscriber: An error occurred during the beforeEntityCreate process:', error); + } + } - entity?.stages?.forEach((stage) => { - Object.assign(stage, pipelineId, { index: ++index }); - }); - } catch (error) { - console.error('PipelineSubscriber: An error occurred during the beforeEntityCreate process:', error); - } - } + /** + * Called after a Pipeline entity is inserted into the database. This method performs + * additional operations defined in the __after_fetch method on the newly created entity. + * + * @param entity The Pipeline entity that has been created. + * @returns {Promise} A promise that resolves when the post-creation processing is complete. + */ + async afterEntityCreate(entity: Pipeline): Promise { + try { + this.__after_fetch(entity); + } catch (error) { + console.error('PipelineSubscriber: An error occurred during the afterEntityCreate process:', error); + } + } - /** - * Called after a Pipeline entity is inserted into the database. This method performs - * additional operations defined in the __after_fetch method on the newly created entity. - * - * @param entity The Pipeline entity that has been created. - * @returns {Promise} A promise that resolves when the post-creation processing is complete. - */ - async afterEntityCreate(entity: Pipeline): Promise { - try { - this.__after_fetch(entity); - } catch (error) { - console.error('PipelineSubscriber: An error occurred during the afterEntityCreate process:', error); - } - } - - /*** - * Internal method to be used after fetching the Pipeline entity. - * - * @param entity - The fetched Pipeline entity. - */ - private __after_fetch(entity: Pipeline): void { - if (entity.stages) { - entity.stages.sort(({ index: a }, { index: b }) => a - b); - } - } + /*** + * Internal method to be used after fetching the Pipeline entity. + * + * @param entity - The fetched Pipeline entity. + */ + private __after_fetch(entity: Pipeline): void { + if (entity.stages) { + entity.stages.sort(({ index: a }, { index: b }) => a - b); + } + } } diff --git a/packages/core/src/product/commands/handlers/product.create.handler.ts b/packages/core/src/product/commands/handlers/product.create.handler.ts index d459a1cb6da..8a52d764247 100644 --- a/packages/core/src/product/commands/handlers/product.create.handler.ts +++ b/packages/core/src/product/commands/handlers/product.create.handler.ts @@ -8,12 +8,11 @@ import { ProductOptionGroup, ProductOptionTranslation, ProductOptionGroupTranslation -} from 'core'; -import { ProductOptionGroupService } from 'product-option/product-option-group.service'; +} from '../../../core/entities/internal'; +import { ProductOptionGroupService } from '../../../product-option/product-option-group.service'; @CommandHandler(ProductCreateCommand) -export class ProductCreateHandler - implements ICommandHandler { +export class ProductCreateHandler implements ICommandHandler { constructor( private productOptionService: ProductOptionService, private productService: ProductService, @@ -39,22 +38,17 @@ export class ProductCreateHandler ...optionInput }); - const optionsTranslationEntites = await Promise.all( + const optionsTranslationEntities = await Promise.all( option.translations.map((optionTranslation) => { - let optionTranslationEntity = Object.assign( - new ProductOptionTranslation(), - { ...optionTranslation } - ); - return this.productOptionService.saveProductOptionTranslation( - optionTranslationEntity - ); + let optionTranslationEntity = Object.assign(new ProductOptionTranslation(), { + ...optionTranslation + }); + return this.productOptionService.saveProductOptionTranslation(optionTranslationEntity); }) ); - option.translations = optionsTranslationEntites; - const optionEntity = await this.productOptionService.save( - option - ); + option.translations = optionsTranslationEntities; + const optionEntity = await this.productOptionService.save(option); if (optionEntity) { newGroup.options.push(optionEntity); @@ -62,29 +56,22 @@ export class ProductCreateHandler } //save group translations - const groupTranslationsEntites = Promise.all( + const groupTranslationsEntities = Promise.all( group.translations.map((groupTranslation) => { - let groupTranslationObj = Object.assign( - new ProductOptionGroupTranslation(), - { ...groupTranslation } - ); - return this.productOptionsGroupService.createTranslation( - groupTranslationObj - ); + let groupTranslationObj = Object.assign(new ProductOptionGroupTranslation(), { + ...groupTranslation + }); + return this.productOptionsGroupService.createTranslation(groupTranslationObj); }) ); - newGroup.translations = await groupTranslationsEntites; + newGroup.translations = await groupTranslationsEntities; return newGroup; }) ); - product.optionGroups = await this.productOptionsGroupService.saveBulk( - optionsGroupsCreate - ); - const updatedProduct = await this.productService.saveProduct( - product as any - ); + product.optionGroups = await this.productOptionsGroupService.saveBulk(optionsGroupsCreate); + const updatedProduct = await this.productService.saveProduct(product as any); return updatedProduct; } diff --git a/packages/core/src/product/commands/handlers/product.delete.handler.ts b/packages/core/src/product/commands/handlers/product.delete.handler.ts index 5b0243578e4..1e88c824fa2 100644 --- a/packages/core/src/product/commands/handlers/product.delete.handler.ts +++ b/packages/core/src/product/commands/handlers/product.delete.handler.ts @@ -1,16 +1,15 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { DeleteResult } from 'typeorm'; import { ProductDeleteCommand } from '../product.delete.command'; import { ProductService } from '../../product.service'; import { ProductVariantService } from '../../../product-variant/product-variant.service'; -import { DeleteResult } from 'typeorm'; import { ProductVariantSettingService } from '../../../product-setting/product-setting.service'; import { ProductVariantPriceService } from '../../../product-variant-price/product-variant-price.service'; -import { ProductOptionService } from 'product-option/product-option.service'; -import { ProductOptionGroupService } from 'product-option/product-option-group.service'; +import { ProductOptionService } from '../../../product-option/product-option.service'; +import { ProductOptionGroupService } from '../../../product-option/product-option-group.service'; @CommandHandler(ProductDeleteCommand) -export class ProductDeleteHandler - implements ICommandHandler { +export class ProductDeleteHandler implements ICommandHandler { constructor( private productService: ProductService, private productOptionService: ProductOptionService, @@ -20,9 +19,7 @@ export class ProductDeleteHandler private productVariantPricesService: ProductVariantPriceService ) {} - public async execute( - command?: ProductDeleteCommand - ): Promise { + public async execute(command?: ProductDeleteCommand): Promise { const { productId } = command; const product = await this.productService.findOneByOptions({ @@ -44,27 +41,21 @@ export class ProductDeleteHandler for await (const optionGroup of optionGroups) { optionGroup.options.forEach(async (option) => { - await this.productOptionService.deleteOptionTranslationsBulk( - option.translations - ); + await this.productOptionService.deleteOptionTranslationsBulk(option.translations); }); await this.productOptionService.deleteBulk(optionGroup.options); } for await (const group of optionGroups) { - await this.productOptionsGroupService.deleteGroupTranslationsBulk( - group.translations - ); + await this.productOptionsGroupService.deleteGroupTranslationsBulk(group.translations); } await this.productOptionsGroupService.deleteBulk(optionGroups); const deleteRes = [ await this.productVariantService.deleteMany(product.variants), - await this.productVariantSettingsService.deleteMany( - settingsToDelete - ), + await this.productVariantSettingsService.deleteMany(settingsToDelete), await this.productVariantPricesService.deleteMany(pricesToDelete), await this.productService.delete(product.id) ]; diff --git a/packages/core/src/product/commands/handlers/product.update.handler.ts b/packages/core/src/product/commands/handlers/product.update.handler.ts index 412dd74017a..dd6cf9637d1 100644 --- a/packages/core/src/product/commands/handlers/product.update.handler.ts +++ b/packages/core/src/product/commands/handlers/product.update.handler.ts @@ -4,12 +4,7 @@ import { ProductOptionService } from '../../../product-option/product-option.ser import { Product } from '../../product.entity'; import { ProductUpdateCommand } from '../product.update.command'; import { ProductOptionGroupService } from 'product-option/product-option-group.service'; -import { - ProductOption, - ProductOptionGroup, - ProductOptionTranslation, - ProductOptionGroupTranslation -} from 'core'; +import { ProductOption, ProductOptionGroup, ProductOptionTranslation, ProductOptionGroupTranslation } from 'core'; import { IProductOptionGroupTranslatable, IProductOptionTranslatable, @@ -18,8 +13,7 @@ import { } from '@gauzy/contracts'; @CommandHandler(ProductUpdateCommand) -export class ProductUpdateHandler - implements ICommandHandler { +export class ProductUpdateHandler implements ICommandHandler { constructor( private productOptionService: ProductOptionService, private productService: ProductService, @@ -30,25 +24,17 @@ export class ProductUpdateHandler const { productUpdateRequest } = command; const optionDeleteInputs = productUpdateRequest.optionDeleteInputs; - const optionGroupCreateInputs = - productUpdateRequest.optionGroupCreateInputs; - const optionGroupUpdateInputs = - productUpdateRequest.optionGroupUpdateInputs; - const optionGroupDeleteInputs = - productUpdateRequest.optionGroupDeleteInputs; - - const product = await this.productService.findById( - productUpdateRequest.id, - { relations: ['optionGroups'] } - ); + const optionGroupCreateInputs = productUpdateRequest.optionGroupCreateInputs; + const optionGroupUpdateInputs = productUpdateRequest.optionGroupUpdateInputs; + const optionGroupDeleteInputs = productUpdateRequest.optionGroupDeleteInputs; + + const product = await this.productService.findById(productUpdateRequest.id, { relations: ['optionGroups'] }); /** * delete options */ for await (const option of optionDeleteInputs) { - await this.productOptionService.deleteOptionTranslationsBulk( - option.translations - ); + await this.productOptionService.deleteOptionTranslationsBulk(option.translations); } await this.productOptionService.deleteBulk(optionDeleteInputs); @@ -56,181 +42,133 @@ export class ProductUpdateHandler * delete option groups */ for await (const group of optionGroupDeleteInputs) { - await this.productOptionsGroupService.deleteGroupTranslationsBulk( - group.translations - ); + await this.productOptionsGroupService.deleteGroupTranslationsBulk(group.translations); } - await this.productOptionsGroupService.deleteBulk( - optionGroupDeleteInputs - ); + await this.productOptionsGroupService.deleteBulk(optionGroupDeleteInputs); /** * create new option group */ const optionsGroupsCreate: IProductOptionGroupTranslatable[] = await Promise.all( - optionGroupCreateInputs.map( - async (group: IProductOptionGroupTranslatable) => { - let newGroup = new ProductOptionGroup(); - newGroup.name = group.name; - newGroup.translations = []; - newGroup.options = []; - - /** - * save group options with their translations - */ - for await (const optionInput of group.options) { - const option = Object.assign(new ProductOption(), { - ...optionInput - }); - - const optionsTranslationEntites = await Promise.all( - option.translations.map((optionTranslation) => { - let optionTranslationEntity = Object.assign( - new ProductOptionTranslation(), - { ...optionTranslation } - ); - return this.productOptionService.saveProductOptionTranslation( - optionTranslationEntity - ); - }) - ); - - option.translations = optionsTranslationEntites; - const optionEntity = await this.productOptionService.save( - option - ); - - if (optionEntity) { - newGroup.options.push(optionEntity); - } - } - - /** - * save group translations. - */ - const groupTranslationsEntites = Promise.all( - group.translations.map((groupTranslation) => { - let groupTranslationObj = Object.assign( - new ProductOptionGroupTranslation(), - { ...groupTranslation } - ); - return this.productOptionsGroupService.createTranslation( - groupTranslationObj - ); + optionGroupCreateInputs.map(async (group: IProductOptionGroupTranslatable) => { + let newGroup = new ProductOptionGroup(); + newGroup.name = group.name; + newGroup.translations = []; + newGroup.options = []; + + /** + * save group options with their translations + */ + for await (const optionInput of group.options) { + const option = Object.assign(new ProductOption(), { + ...optionInput + }); + + const optionsTranslationEntities = await Promise.all( + option.translations.map((optionTranslation) => { + let optionTranslationEntity = Object.assign(new ProductOptionTranslation(), { + ...optionTranslation + }); + return this.productOptionService.saveProductOptionTranslation(optionTranslationEntity); }) ); - newGroup.translations = (await groupTranslationsEntites) as any; - return newGroup; + option.translations = optionsTranslationEntities; + const optionEntity = await this.productOptionService.save(option); + + if (optionEntity) { + newGroup.options.push(optionEntity); + } } - ) + + /** + * save group translations. + */ + const groupTranslationsEntities = Promise.all( + group.translations.map((groupTranslation) => { + let groupTranslationObj = Object.assign(new ProductOptionGroupTranslation(), { + ...groupTranslation + }); + return this.productOptionsGroupService.createTranslation(groupTranslationObj); + }) + ); + + newGroup.translations = (await groupTranslationsEntities) as any; + return newGroup; + }) ); /** * update product option groups */ - const optionGroupsUpdate: - | IProductOptionGroupTranslatable[] - | any = await Promise.all( - optionGroupUpdateInputs.map( - async (group: IProductOptionGroupTranslatable) => { - for await (let option of group.options) { - let isNewOption = false; - - if (!option.id) { - option = Object.assign(new ProductOption(), { - ...option - }); - isNewOption = true; - } - - let existingOption = isNewOption - ? null - : await this.productOptionService.findOneByIdString( - option.id - ); - - const optionsTranslationEntites = await Promise.all( - option.translations.map( - async ( - optionTranslation: IProductOptionTranslation - ) => { - if ( - this.productOptionTranslationUpdated( - existingOption, - optionTranslation - ) || - !optionTranslation.id - ) { - return this.productOptionService.saveProductOptionTranslation( - { - reference: option.id || null, - ...optionTranslation - } as any - ); - } - } - ) - ); - - option.translations = option.translations.concat( - (await optionsTranslationEntites).filter( - (tr) => !!tr - ) - ); - const optionEntity = await this.productOptionService.save( - option - ); - - if (optionEntity && isNewOption) { - group.options.push(optionEntity); - } + const optionGroupsUpdate: IProductOptionGroupTranslatable[] | any = await Promise.all( + optionGroupUpdateInputs.map(async (group: IProductOptionGroupTranslatable) => { + for await (let option of group.options) { + let isNewOption = false; + + if (!option.id) { + option = Object.assign(new ProductOption(), { + ...option + }); + isNewOption = true; } - /** - * save group translations. - */ - let existingGroup = await this.productOptionsGroupService.findOneByIdString( - group.id - ); + let existingOption = isNewOption + ? null + : await this.productOptionService.findOneByIdString(option.id); - const groupTranslationsEntites = Promise.all( - group.translations.map((groupTranslation) => { + const optionsTranslationEntities = await Promise.all( + option.translations.map(async (optionTranslation: IProductOptionTranslation) => { if ( - this.productOptionGroupTranslationUpdated( - existingGroup, - groupTranslation - ) + this.productOptionTranslationUpdated(existingOption, optionTranslation) || + !optionTranslation.id ) { - return this.productOptionsGroupService.createTranslation( - { - reference: group.id || null, - ...groupTranslation - } as any - ); + return this.productOptionService.saveProductOptionTranslation({ + reference: option.id || null, + ...optionTranslation + } as any); } }) ); - group.translations = existingGroup.translations.concat( - (await groupTranslationsEntites).filter( - (tr) => !!tr - ) as any + option.translations = option.translations.concat( + (await optionsTranslationEntities).filter((tr) => !!tr) ); + const optionEntity = await this.productOptionService.save(option); - return group; + if (optionEntity && isNewOption) { + group.options.push(optionEntity); + } } - ) - ); - let newProductOptions = await this.productOptionsGroupService.saveBulk( - optionsGroupsCreate as any - ); - await this.productOptionsGroupService.saveBulk( - optionGroupsUpdate as any + /** + * save group translations. + */ + let existingGroup = await this.productOptionsGroupService.findOneByIdString(group.id); + + const groupTranslationsEntities = Promise.all( + group.translations.map((groupTranslation) => { + if (this.productOptionGroupTranslationUpdated(existingGroup, groupTranslation)) { + return this.productOptionsGroupService.createTranslation({ + reference: group.id || null, + ...groupTranslation + } as any); + } + }) + ); + + group.translations = existingGroup.translations.concat( + (await groupTranslationsEntities).filter((tr) => !!tr) as any + ); + + return group; + }) ); + let newProductOptions = await this.productOptionsGroupService.saveBulk(optionsGroupsCreate as any); + await this.productOptionsGroupService.saveBulk(optionGroupsUpdate as any); + product.optionGroups = product.optionGroups.concat(newProductOptions); product.productCategory = productUpdateRequest.category; product.productTypeId = productUpdateRequest.type; @@ -238,17 +176,13 @@ export class ProductUpdateHandler const productTranslations = await Promise.all( productUpdateRequest.translations.map((optionTranslation) => { - return this.productService.saveProductTranslation( - optionTranslation - ); + return this.productService.saveProductTranslation(optionTranslation); }) ); product.translations = productTranslations; - const updatedProduct = await this.productService.saveProduct( - product as any - ); + const updatedProduct = await this.productService.saveProduct(product as any); return updatedProduct; } @@ -263,17 +197,14 @@ export class ProductUpdateHandler if (!productOption) return true; let currentTranslation: IProductOptionTranslation = productOption.translations.find( - (translation) => - translation.languageCode == - productOptionTranslation.languageCode + (translation) => translation.languageCode == productOptionTranslation.languageCode ); if (!currentTranslation) return true; if ( currentTranslation.name !== productOptionTranslation.name || - currentTranslation.description !== - productOptionTranslation.description + currentTranslation.description !== productOptionTranslation.description ) { return true; } @@ -291,8 +222,7 @@ export class ProductUpdateHandler if (!optionGroup) return false; let currentTranslation: IProductOptionGroupTranslation = optionGroup.translations.find( - (translation) => - translation.languageCode == optionGroupTranslation.languageCode + (translation) => translation.languageCode == optionGroupTranslation.languageCode ); if (!currentTranslation) return true; diff --git a/packages/core/src/public-share/organization/public-organization.service.ts b/packages/core/src/public-share/organization/public-organization.service.ts index d3759183dd3..e0a9e36d828 100644 --- a/packages/core/src/public-share/organization/public-organization.service.ts +++ b/packages/core/src/public-share/organization/public-organization.service.ts @@ -3,13 +3,12 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsWhere } from 'typeorm'; import { Organization, OrganizationContact, OrganizationProject } from './../../core/entities/internal'; -import { TypeOrmOrganizationRepository } from 'organization/repository/type-orm-organization.repository'; -import { TypeOrmOrganizationContactRepository } from 'organization-contact/repository/type-orm-organization-contact.repository'; -import { TypeOrmOrganizationProjectRepository } from 'organization-project/repository/type-orm-organization-project.repository'; +import { TypeOrmOrganizationRepository } from '../../organization/repository/type-orm-organization.repository'; +import { TypeOrmOrganizationContactRepository } from '../../organization-contact/repository/type-orm-organization-contact.repository'; +import { TypeOrmOrganizationProjectRepository } from '../../organization-project/repository/type-orm-organization-project.repository'; @Injectable() export class PublicOrganizationService { - constructor( @InjectRepository(Organization) private typeOrmOrganizationRepository: TypeOrmOrganizationRepository, @@ -18,8 +17,8 @@ export class PublicOrganizationService { private typeOrmOrganizationContactRepository: TypeOrmOrganizationContactRepository, @InjectRepository(OrganizationProject) - private typeOrmOrganizationProjectRepository: TypeOrmOrganizationProjectRepository, - ) { } + private typeOrmOrganizationProjectRepository: TypeOrmOrganizationProjectRepository + ) {} /** * GET organization by profile link @@ -28,10 +27,7 @@ export class PublicOrganizationService { * @param relations * @returns */ - async findOneByProfileLink( - where: FindOptionsWhere, - relations: string[] - ): Promise { + async findOneByProfileLink(where: FindOptionsWhere, relations: string[]): Promise { try { return await this.typeOrmOrganizationRepository.findOneOrFail({ where, @@ -65,9 +61,7 @@ export class PublicOrganizationService { * @param options * @returns */ - async findPublicClientCountsByOrganization( - options: FindOptionsWhere - ): Promise { + async findPublicClientCountsByOrganization(options: FindOptionsWhere): Promise { try { return await this.typeOrmOrganizationContactRepository.countBy(options); } catch (error) { @@ -81,9 +75,7 @@ export class PublicOrganizationService { * @param options * @returns */ - async findPublicProjectCountsByOrganization( - options: FindOptionsWhere - ): Promise { + async findPublicProjectCountsByOrganization(options: FindOptionsWhere): Promise { try { return await this.typeOrmOrganizationProjectRepository.countBy(options); } catch (error) { diff --git a/packages/core/src/shared/decorators/user.decorator.ts b/packages/core/src/shared/decorators/user.decorator.ts index 00f091d614c..77786f21907 100644 --- a/packages/core/src/shared/decorators/user.decorator.ts +++ b/packages/core/src/shared/decorators/user.decorator.ts @@ -1,8 +1,13 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; -export const UserDecorator = createParamDecorator( - (data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - return request.user || {}; - }, -); \ No newline at end of file +/** + * Custom decorator to extract user information from the request object. + * + * @param data - Optional data parameter (not used in this implementation). + * @param ctx - The execution context from which to extract the request object. + * @returns The user object from the request, or an empty object if not found. + */ +export const UserDecorator = createParamDecorator((data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.user || {}; +}); diff --git a/packages/core/src/tags/commands/automation-label.sync.command.ts b/packages/core/src/tags/commands/automation-label.sync.command.ts index a5272887569..24c402a3999 100644 --- a/packages/core/src/tags/commands/automation-label.sync.command.ts +++ b/packages/core/src/tags/commands/automation-label.sync.command.ts @@ -1,9 +1,8 @@ -import { IIntegrationMapSyncEntity, ITagCreateInput, ITagUpdateInput, IntegrationEntity } from "@gauzy/contracts"; +import { IIntegrationMapSyncEntity, ITagCreateInput, ITagUpdateInput, IntegrationEntity } from '@gauzy/contracts'; export class AutomationLabelSyncCommand { - - constructor( - public readonly input: IIntegrationMapSyncEntity, - public readonly entity: IntegrationEntity - ) { } + constructor( + public readonly input: IIntegrationMapSyncEntity, + public readonly entity: IntegrationEntity + ) {} } diff --git a/packages/core/src/tags/commands/handlers/automation-label.sync.handler.ts b/packages/core/src/tags/commands/handlers/automation-label.sync.handler.ts index 65140e9629d..f0e9942d12b 100644 --- a/packages/core/src/tags/commands/handlers/automation-label.sync.handler.ts +++ b/packages/core/src/tags/commands/handlers/automation-label.sync.handler.ts @@ -1,8 +1,8 @@ import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; import { InjectRepository } from '@nestjs/typeorm'; import { IOrganization, ITag, ITagCreateInput, ITagUpdateInput } from '@gauzy/contracts'; -import { IntegrationMap } from 'core/entities/internal'; -import { RequestContext } from 'core/context'; +import { IntegrationMap } from '../../../core/entities/internal'; +import { RequestContext } from '../../../core/context'; import { Tag } from './../../tag.entity'; import { TagService } from './../../tag.service'; import { AutomationLabelSyncCommand } from './../automation-label.sync.command'; @@ -11,7 +11,6 @@ import { TypeOrmIntegrationMapRepository } from '../../../integration-map/reposi @CommandHandler(AutomationLabelSyncCommand) export class AutomationLabelSyncHandler implements ICommandHandler { - constructor( @InjectRepository(Tag) private readonly typeOrmTagRepository: TypeOrmTagRepository, @@ -20,7 +19,7 @@ export class AutomationLabelSyncHandler implements ICommandHandler { try { @@ -50,15 +49,17 @@ export class AutomationLabelSyncHandler implements ICommandHandler { + async createTag( + options: { + organizationId: IOrganization['id']; + tenantId: IOrganization['tenantId']; + }, + entity: ITagCreateInput | ITagUpdateInput + ): Promise { try { // Create a new tag with the provided entity data const newTag = this.typeOrmTagRepository.create({ @@ -115,10 +118,7 @@ export class AutomationLabelSyncHandler implements ICommandHandler { + async updateTag(id: ITagUpdateInput['id'], entity: ITagUpdateInput): Promise { try { // Find the existing tag by its ID const existingTag = await this._tagService.findOneByIdString(id); diff --git a/packages/core/src/tags/dto/tag-query-by-level.dto.ts b/packages/core/src/tags/dto/tag-query-by-level.dto.ts index 2d208edd462..411d6bfe83c 100644 --- a/packages/core/src/tags/dto/tag-query-by-level.dto.ts +++ b/packages/core/src/tags/dto/tag-query-by-level.dto.ts @@ -1,9 +1,8 @@ import { IntersectionType, PartialType } from '@nestjs/swagger'; import { ITagFindInput } from '@gauzy/contracts'; import { TenantOrganizationBaseDTO } from '../../core/dto'; -import { RelationsQueryDTO } from 'shared/dto'; +import { RelationsQueryDTO } from '../../shared/dto'; -export class TagQueryByLevelDTO extends IntersectionType( - PartialType(TenantOrganizationBaseDTO), - RelationsQueryDTO -) implements ITagFindInput { } +export class TagQueryByLevelDTO + extends IntersectionType(PartialType(TenantOrganizationBaseDTO), RelationsQueryDTO) + implements ITagFindInput {} diff --git a/packages/core/src/tags/index.ts b/packages/core/src/tags/index.ts index 82cd8055ac1..b27c8fd2db8 100644 --- a/packages/core/src/tags/index.ts +++ b/packages/core/src/tags/index.ts @@ -1,2 +1,5 @@ +export * from './tag.module'; +export * from './tag.service'; export * from './tag.types'; -export * from './dto'; \ No newline at end of file +export * from './dto'; +export * from './commands'; diff --git a/packages/core/src/tags/tag.module.ts b/packages/core/src/tags/tag.module.ts index 5b6f70c5412..47758d87e02 100644 --- a/packages/core/src/tags/tag.module.ts +++ b/packages/core/src/tags/tag.module.ts @@ -4,7 +4,7 @@ import { RouterModule } from '@nestjs/core'; import { CqrsModule } from '@nestjs/cqrs'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { RolePermissionModule } from '../role-permission/role-permission.module'; -import { IntegrationMap } from 'core/entities/internal'; +import { IntegrationMap } from '../core/entities/internal'; import { TagController } from './tag.controller'; import { TagService } from './tag.service'; import { Tag } from './tag.entity'; diff --git a/packages/core/src/tags/tag.service.ts b/packages/core/src/tags/tag.service.ts index a5946272bf6..b89c70a06e5 100644 --- a/packages/core/src/tags/tag.service.ts +++ b/packages/core/src/tags/tag.service.ts @@ -93,7 +93,7 @@ export class TagService extends TenantAwareCrudService { if (customFields.length > 0) { customFields.forEach((field) => { if (field.relationType === 'many-to-many') { - query.leftJoin(`${query.alias}.customFields.${field.propertyPath}`, field.propertyPath); + query.leftJoin(`${query.alias}.customFields.${field.name}`, field.name); } }); } @@ -137,8 +137,8 @@ export class TagService extends TenantAwareCrudService { if (customFields.length > 0) { customFields.forEach((field) => { if (field.relationType === 'many-to-many') { - const selectionAliasName = `${field.propertyPath}_counter`; - query.addSelect(`CAST(COUNT(${field.propertyPath}.id) AS INTEGER)`, selectionAliasName); + const selectionAliasName = `${field.name}_counter`; + query.addSelect(`CAST(COUNT(${field.name}.id) AS INTEGER)`, selectionAliasName); } }); } diff --git a/packages/core/src/tags/tag.types.ts b/packages/core/src/tags/tag.types.ts index 5e00c6aceae..2a44504361f 100644 --- a/packages/core/src/tags/tag.types.ts +++ b/packages/core/src/tags/tag.types.ts @@ -1,11 +1,11 @@ -import { Tag } from "./tag.entity"; +import { Tag } from './tag.entity'; /** * Represents an entity that can be tagged with multiple tags. */ export interface Taggable { - /** - * An array of tags associated with the entity. - */ - tags?: Tag[]; + /** + * An array of tags associated with the entity. + */ + tags?: Tag[]; } diff --git a/packages/core/src/tasks/commands/automation-task.sync.command.ts b/packages/core/src/tasks/commands/automation-task.sync.command.ts index dc140268172..4242f99ab6e 100644 --- a/packages/core/src/tasks/commands/automation-task.sync.command.ts +++ b/packages/core/src/tasks/commands/automation-task.sync.command.ts @@ -1,9 +1,8 @@ -import { IIntegrationMapSyncEntity, ITaskCreateInput, ITaskUpdateInput, IntegrationEntity } from "@gauzy/contracts"; +import { IIntegrationMapSyncEntity, ITaskCreateInput, ITaskUpdateInput, IntegrationEntity } from '@gauzy/contracts'; export class AutomationTaskSyncCommand { - - constructor( - public readonly input: IIntegrationMapSyncEntity, - public readonly entity: IntegrationEntity - ) { } + constructor( + public readonly input: IIntegrationMapSyncEntity, + public readonly entity: IntegrationEntity + ) {} } diff --git a/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts b/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts index 9edbb204104..c3b465bd13e 100644 --- a/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts +++ b/packages/core/src/tasks/commands/handlers/automation-task.sync.handler.ts @@ -1,25 +1,18 @@ import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; import { InjectRepository } from '@nestjs/typeorm'; -import { - IIntegrationMap, - IOrganization, - IOrganizationProject, - ITask, - ITaskCreateInput, - ITaskUpdateInput -} from '@gauzy/contracts'; -import { RequestContext } from 'core/context'; -import { IntegrationMap, TaskStatus } from 'core/entities/internal'; +import * as chalk from 'chalk'; +import { ID, IIntegrationMap, ITask, ITaskCreateInput, ITaskUpdateInput } from '@gauzy/contracts'; +import { RequestContext } from '../../../core/context'; +import { IntegrationMap, TaskStatus } from '../../../core/entities/internal'; import { AutomationTaskSyncCommand } from './../automation-task.sync.command'; import { TaskService } from './../../task.service'; import { Task } from './../../task.entity'; import { TypeOrmIntegrationMapRepository } from '../../../integration-map/repository/type-orm-integration-map.repository'; -import { TypeOrmTaskStatusRepository } from 'tasks/statuses/repository/type-orm-task-status.repository'; -import { TypeOrmTaskRepository } from 'tasks/repository/type-orm-task.repository'; +import { TypeOrmTaskStatusRepository } from '../../statuses/repository/type-orm-task-status.repository'; +import { TypeOrmTaskRepository } from '../../repository/type-orm-task.repository'; @CommandHandler(AutomationTaskSyncCommand) export class AutomationTaskSyncHandler implements ICommandHandler { - constructor( @InjectRepository(Task) private readonly typeOrmTaskRepository: TypeOrmTaskRepository, @@ -31,8 +24,14 @@ export class AutomationTaskSyncHandler implements ICommandHandler} - The integration map after synchronization. + */ async execute(command: AutomationTaskSyncCommand): Promise { try { const { input } = command; @@ -40,13 +39,17 @@ export class AutomationTaskSyncHandler implements ICommandHandler { + async createTask( + options: { projectId: ID; organizationId: ID; tenantId: ID }, + entity: ITaskCreateInput | ITaskUpdateInput + ): Promise { try { // Retrieve the maximum task number for the project const maxNumber = await this._taskService.getMaxTaskNumberByProject(options); @@ -130,12 +128,14 @@ export class AutomationTaskSyncHandler implements ICommandHandler { + async updateTask(id: ITaskUpdateInput['id'], entity: ITaskUpdateInput): Promise { try { // Find the existing task by its ID const existingTask = await this._taskService.findOneByIdString(id); @@ -165,7 +162,7 @@ export class AutomationTaskSyncHandler implements ICommandHandler { private readonly _eventBus: EventBus, private readonly _taskService: TaskService, private readonly _organizationProjectService: OrganizationProjectService - ) { } + ) {} + /** + * Executes the task creation command, handling project association and event publishing. + * + * @param command The command containing task creation input and event triggering flag. + * @returns The created task. + */ public async execute(command: TaskCreateCommand): Promise { try { + // Destructure input and triggered event flag from the command const { input, triggeredEvent } = command; let { organizationId, project } = input; + + // Retrieve current tenant ID from request context or use input tenant ID const tenantId = RequestContext.currentTenantId() || input.tenantId; - /** If project found then use project name as a task prefix */ + // If input contains project ID, fetch project details if (input.projectId) { const { projectId } = input; project = await this._organizationProjectService.findOneByIdString(projectId); } + // Determine project ID and task prefix based on project existence const projectId = project ? project.id : null; const taskPrefix = project ? project.name.substring(0, 3) : null; + // Retrieve the maximum task number for the specified project const maxNumber = await this._taskService.getMaxTaskNumberByProject({ tenantId, organizationId, - projectId, + projectId }); + // Create the task with incremented number, prefix, and other details const createdTask = await this._taskService.create({ ...input, number: maxNumber + 1, prefix: taskPrefix, tenantId, - organizationId, + organizationId }); - // The "2 Way Sync Triggered Event" for Synchronization + // Publish a task created event if triggeredEvent flag is set if (triggeredEvent) { - this._eventBus.publish(new TaskCreatedEvent(createdTask)); + // Publish the task created event + const ctx = RequestContext.currentRequestContext(); // Get current request context; + const event = new TaskEvent(ctx, createdTask, BaseEntityEventTypeEnum.CREATED, input); + this._eventBus.publish(event); // Publish the event using EventBus } - return createdTask; + return createdTask; // Return the created task } catch (error) { + // Handle errors during task creation this.logger.error(`Error while creating task: ${error.message}`, error.message); throw new HttpException({ message: error?.message, error }, HttpStatus.BAD_REQUEST); } 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 46570b19ddb..fb0c8396997 100644 --- a/packages/core/src/tasks/commands/handlers/task-update.handler.ts +++ b/packages/core/src/tasks/commands/handlers/task-update.handler.ts @@ -1,42 +1,47 @@ import { HttpException, HttpStatus, Logger } from '@nestjs/common'; -import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs'; -import { ITask, ITaskUpdateInput } from '@gauzy/contracts'; -import { RequestContext } from 'core/context'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { ID, ITask, ITaskUpdateInput } from '@gauzy/contracts'; +import { TaskEvent } from '../../../event-bus/events'; +import { EventBus } from '../../../event-bus/event-bus'; +import { BaseEntityEventTypeEnum } from '../../../event-bus/base-entity-event'; +import { RequestContext } from '../../../core/context'; import { TaskService } from '../../task.service'; import { TaskUpdateCommand } from '../task-update.command'; -import { TaskUpdatedEvent } from './../../events/task-updated.event'; @CommandHandler(TaskUpdateCommand) export class TaskUpdateHandler implements ICommandHandler { private readonly logger = new Logger('TaskUpdateHandler'); - constructor( - private readonly _eventBus: EventBus, - private readonly _taskService: TaskService - ) { } + constructor(private readonly _eventBus: EventBus, private readonly _taskService: TaskService) {} + /** + * Executes the TaskUpdateCommand. + * + * @param command - The command containing the task ID, update data, and a flag indicating whether to trigger an event. + * @returns The updated task. + */ public async execute(command: TaskUpdateCommand): Promise { + // Destructure the command object to get the task ID, input data, and the triggered event flag const { id, input, triggeredEvent } = command; + + // Call the update method with the extracted parameters and return the updated task return await this.update(id, input, triggeredEvent); } /** * Update task, if already exist * - * @param id - * @param request - * @returns + * @param id - The ID of the task to update + * @param input - The data to update the task with + * @param triggeredEvent - Flag to indicate if an event should be triggered + * @returns The updated task */ - public async update( - id: ITask['id'], - request: ITaskUpdateInput, - triggeredEvent: boolean - ): Promise { + public async update(id: ID, input: ITaskUpdateInput, triggeredEvent: boolean): Promise { try { - const tenantId = RequestContext.currentTenantId() || request.tenantId; + const tenantId = RequestContext.currentTenantId() || input.tenantId; const task = await this._taskService.findOneByIdString(id); - if (request.projectId && request.projectId !== task.projectId) { + if (input.projectId && input.projectId !== task.projectId) { const { organizationId, projectId } = task; // Get the maximum task number for the project @@ -55,13 +60,16 @@ export class TaskUpdateHandler implements ICommandHandler { // Update the task with the provided data const updatedTask = await this._taskService.create({ - ...request, + ...input, id }); // The "2 Way Sync Triggered Event" for Synchronization if (triggeredEvent) { - this._eventBus.publish(new TaskUpdatedEvent(updatedTask)); + // Publish the task created event + const ctx = RequestContext.currentRequestContext(); // Get current request context; + const event = new TaskEvent(ctx, updatedTask, BaseEntityEventTypeEnum.UPDATED, input); + this._eventBus.publish(event); // Publish the event using EventBus } return updatedTask; @@ -70,5 +78,4 @@ export class TaskUpdateHandler implements ICommandHandler { throw new HttpException({ message: error?.message, error }, HttpStatus.BAD_REQUEST); } } - } diff --git a/packages/core/src/tasks/daily-plan/daily-plan.module.ts b/packages/core/src/tasks/daily-plan/daily-plan.module.ts index fb15f05fbd5..8d4e9f41d63 100644 --- a/packages/core/src/tasks/daily-plan/daily-plan.module.ts +++ b/packages/core/src/tasks/daily-plan/daily-plan.module.ts @@ -4,7 +4,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { TypeOrmModule } from '@nestjs/typeorm'; import { DailyPlanService } from './daily-plan.service'; import { DailyPlanController } from './daily-plan.controller'; -import { RolePermissionModule } from 'role-permission'; +import { RolePermissionModule } from '../../role-permission/role-permission.module'; import { DailyPlan } from './daily-plan.entity'; import { EmployeeModule } from '../../employee/employee.module'; import { TaskModule } from '../task.module'; @@ -12,9 +12,7 @@ import { TypeOrmDailyPlanRepository } from './repository'; @Module({ imports: [ - RouterModule.register([ - { path: '/daily-plan', module: DailyPlanModule } - ]), + RouterModule.register([{ path: '/daily-plan', module: DailyPlanModule }]), TypeOrmModule.forFeature([DailyPlan]), MikroOrmModule.forFeature([DailyPlan]), RolePermissionModule, @@ -25,4 +23,4 @@ import { TypeOrmDailyPlanRepository } from './repository'; providers: [DailyPlanService, TypeOrmDailyPlanRepository], exports: [TypeOrmModule, MikroOrmModule, DailyPlanService] }) -export class DailyPlanModule { } +export class DailyPlanModule {} diff --git a/packages/core/src/tasks/dto/get-task-by-id.dto.ts b/packages/core/src/tasks/dto/get-task-by-id.dto.ts index 40011e04fd6..8670703f9ad 100644 --- a/packages/core/src/tasks/dto/get-task-by-id.dto.ts +++ b/packages/core/src/tasks/dto/get-task-by-id.dto.ts @@ -1,7 +1,8 @@ import { IGetTaskById } from '@gauzy/contracts'; import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsOptional } from 'class-validator'; -import { OptionParams, Task } from 'core'; +import { Task } from '../../core/entities/internal'; +import { OptionParams } from '../../core/crud'; /** * GET task by Id DTO validation diff --git a/packages/core/src/tasks/events/handlers/index.ts b/packages/core/src/tasks/events/handlers/index.ts deleted file mode 100644 index 87fa74d47a1..00000000000 --- a/packages/core/src/tasks/events/handlers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TaskCreatedEventHandler } from "./task-created.handler"; -import { TaskUpdatedEventHandler } from "./task-updated.handler"; - -export const EventHandlers = [ - TaskCreatedEventHandler, - TaskUpdatedEventHandler, -]; diff --git a/packages/core/src/tasks/events/handlers/task-created.handler.ts b/packages/core/src/tasks/events/handlers/task-created.handler.ts deleted file mode 100644 index 3fc836c4e42..00000000000 --- a/packages/core/src/tasks/events/handlers/task-created.handler.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { HttpException, HttpStatus, Logger } from '@nestjs/common'; -import { CommandBus, EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { RequestContext } from 'core/context'; -import { GithubTaskUpdateOrCreateCommand } from 'integration/github/commands'; -import { TaskCreatedEvent } from '../task-created.event'; - -// Handles event when new task created -@EventsHandler(TaskCreatedEvent) -export class TaskCreatedEventHandler implements IEventHandler { - private readonly logger = new Logger('TaskCreatedEvent'); - - constructor( - private readonly _commandBus: CommandBus - ) { } - - /** - * Handles a `TaskCreatedEvent` by processing the event's input and executing a command if a project ID is present. - * - * @param event - The `TaskCreatedEvent` to handle. - */ - async handle(event: TaskCreatedEvent) { - try { - const { input } = event; - const { organizationId, projectId } = input; - const tenantId = RequestContext.currentTenantId() || input.tenantId; - - // If project found - if (projectId) { - // Prepare a payload for the command - const payload = { - tenantId, - organizationId, - projectId - }; - await this._commandBus.execute( - new GithubTaskUpdateOrCreateCommand(input, payload) - ); - } - } catch (error) { - // Handle errors and return an appropriate error response - this.logger.error('Error while created of a new task', error.message); - throw new HttpException(`Error while created of a new task: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); - } - } -} diff --git a/packages/core/src/tasks/events/handlers/task-updated.handler.ts b/packages/core/src/tasks/events/handlers/task-updated.handler.ts deleted file mode 100644 index 010a6459381..00000000000 --- a/packages/core/src/tasks/events/handlers/task-updated.handler.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { HttpException, HttpStatus, Logger } from '@nestjs/common'; -import { CommandBus, EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { RequestContext } from 'core/context'; -import { GithubTaskUpdateOrCreateCommand } from 'integration/github/commands'; -import { TaskUpdatedEvent } from '../task-updated.event'; - -// Handles event when new task created -@EventsHandler(TaskUpdatedEvent) -export class TaskUpdatedEventHandler implements IEventHandler { - private readonly logger = new Logger('TaskUpdatedEvent'); - - constructor( - private readonly _commandBus: CommandBus - ) { } - - /** - * Handles a `TaskUpdatedEvent` by processing the event's input and executing a command if a project ID is present. - * - * @param event - The `TaskUpdatedEvent` to handle. - */ - async handle(event: TaskUpdatedEvent) { - try { - const { input } = event; - const { organizationId, projectId } = input; - const tenantId = RequestContext.currentTenantId() || input.tenantId; - - // If project found - if (projectId) { - // Prepare a payload for the command - const payload = { - tenantId, - organizationId, - projectId - }; - await this._commandBus.execute( - new GithubTaskUpdateOrCreateCommand(input, payload) - ); - } - } catch (error) { - // Handle errors and return an appropriate error response - this.logger.error('Error while updating of existing task', error.message); - throw new HttpException(`Error while updating of existing task: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); - } - } -} diff --git a/packages/core/src/tasks/events/index.ts b/packages/core/src/tasks/events/index.ts deleted file mode 100644 index 1eff9dadaac..00000000000 --- a/packages/core/src/tasks/events/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './task-created.event'; -export * from './task-updated.event'; diff --git a/packages/core/src/tasks/events/task-created.event.ts b/packages/core/src/tasks/events/task-created.event.ts deleted file mode 100644 index 97633317b67..00000000000 --- a/packages/core/src/tasks/events/task-created.event.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IEvent } from "@nestjs/cqrs"; -import { ITask } from "@gauzy/contracts"; - -export class TaskCreatedEvent implements IEvent { - - constructor( - public readonly input: ITask - ) { } -} diff --git a/packages/core/src/tasks/events/task-updated.event.ts b/packages/core/src/tasks/events/task-updated.event.ts deleted file mode 100644 index 06823b58f71..00000000000 --- a/packages/core/src/tasks/events/task-updated.event.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IEvent } from "@nestjs/cqrs"; -import { ITask } from "@gauzy/contracts"; - -export class TaskUpdatedEvent implements IEvent { - - constructor( - public readonly input: ITask - ) { } -} diff --git a/packages/core/src/tasks/index.ts b/packages/core/src/tasks/index.ts new file mode 100644 index 00000000000..78b2fcaa41d --- /dev/null +++ b/packages/core/src/tasks/index.ts @@ -0,0 +1,3 @@ +export * from './task.module'; +export * from './task.service'; +export * from './commands'; diff --git a/packages/core/src/tasks/task.module.ts b/packages/core/src/tasks/task.module.ts index 5da1c988e0e..119fe2c861d 100644 --- a/packages/core/src/tasks/task.module.ts +++ b/packages/core/src/tasks/task.module.ts @@ -3,10 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { RouterModule } from '@nestjs/core'; import { CqrsModule } from '@nestjs/cqrs'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { IntegrationMap, TaskStatus } from 'core/entities/internal'; +import { EventBusModule } from '../event-bus/event-bus.module'; +import { IntegrationMap, TaskStatus } from '../core/entities/internal'; import { OrganizationProjectModule } from './../organization-project/organization-project.module'; import { CommandHandlers } from './commands/handlers'; -import { EventHandlers } from './events/handlers'; import { RolePermissionModule } from '../role-permission/role-permission.module'; import { UserModule } from './../user/user.module'; import { RoleModule } from './../role/role.module'; @@ -16,22 +16,21 @@ import { TaskService } from './task.service'; import { TaskController } from './task.controller'; import { TypeOrmTaskRepository } from './repository'; -const forFeatureEntities = [Task, TaskStatus, IntegrationMap]; - @Module({ imports: [ RouterModule.register([{ path: '/tasks', module: TaskModule }]), - TypeOrmModule.forFeature(forFeatureEntities), - MikroOrmModule.forFeature(forFeatureEntities), + TypeOrmModule.forFeature([Task, TaskStatus, IntegrationMap]), + MikroOrmModule.forFeature([Task, TaskStatus, IntegrationMap]), RolePermissionModule, forwardRef(() => UserModule), RoleModule, EmployeeModule, OrganizationProjectModule, CqrsModule, + EventBusModule ], controllers: [TaskController], - providers: [TaskService, TypeOrmTaskRepository, ...CommandHandlers, ...EventHandlers], + providers: [TaskService, TypeOrmTaskRepository, ...CommandHandlers], exports: [TypeOrmModule, MikroOrmModule, TaskService, TypeOrmTaskRepository] }) export class TaskModule {} diff --git a/packages/core/src/tasks/task.seed.ts b/packages/core/src/tasks/task.seed.ts index 66dce9d903d..0d83b33e2c8 100644 --- a/packages/core/src/tasks/task.seed.ts +++ b/packages/core/src/tasks/task.seed.ts @@ -5,13 +5,7 @@ import { lastValueFrom, map } from 'rxjs'; import { isNotEmpty } from '@gauzy/common'; import { HttpService } from '@nestjs/axios'; import { AxiosResponse } from 'axios'; -import { GITHUB_API_URL } from '@gauzy/integration-github'; -import { - IGetTaskOptions, - IOrganization, - ITag, - ITenant -} from '@gauzy/contracts'; +import { IGetTaskOptions, IOrganization, ITag, ITenant } from '@gauzy/contracts'; import { Organization, OrganizationProject, @@ -19,15 +13,14 @@ import { Tag, Task, User, - Employee, + Employee } from './../core/entities/internal'; import { prepareSQLQuery as p } from './../database/database.helper'; -export const createDefaultTask = async ( - dataSource: DataSource, - tenant: ITenant, - organization: IOrganization -) => { +// GITHUB API URL +export const GITHUB_API_URL = 'https://api.github.com'; + +export const createDefaultTask = async (dataSource: DataSource, tenant: ITenant, organization: IOrganization) => { const httpService = new HttpService(); console.log(`${GITHUB_API_URL}/repos/ever-co/ever-gauzy/issues`); @@ -43,18 +36,11 @@ export const createDefaultTask = async ( }); labels = uniq(labels, (label) => label.name); - const tags: ITag[] = await createTags( - dataSource, - labels, - tenant, - organization - ); + const tags: ITag[] = await createTags(dataSource, labels, tenant, organization); const defaultProjects = await dataSource.manager.find(OrganizationProject); if (!defaultProjects) { - console.warn( - 'Warning: projects not found, DefaultTasks will not be created' - ); + console.warn('Warning: projects not found, DefaultTasks will not be created'); return; } const teams = await dataSource.manager.find(OrganizationTeam); @@ -67,15 +53,11 @@ export const createDefaultTask = async ( const maxTaskNumber = await getMaxTaskNumberByProject(dataSource, { tenantId: tenant.id, organizationId: organization.id, - projectId: project.id, + projectId: project.id }); const task = new Task(); - task.tags = filter( - tags, - (tag: ITag) => - !!issue.labels.find((label: any) => label.name === tag.name) - ); + task.tags = filter(tags, (tag: ITag) => !!issue.labels.find((label: any) => label.name === tag.name)); task.tenant = tenant; task.organization = organization; task.title = issue.title; @@ -98,10 +80,7 @@ export const createDefaultTask = async ( } }; -export const createRandomTask = async ( - dataSource: DataSource, - tenants: ITenant[] -) => { +export const createRandomTask = async (dataSource: DataSource, tenants: ITenant[]) => { const httpService = new HttpService(); console.log(`${GITHUB_API_URL}/repos/ever-co/ever-gauzy/issues`); @@ -122,65 +101,46 @@ export const createRandomTask = async ( const { id: tenantId } = tenant; const users = await dataSource.manager.find(User, { where: { - tenantId, - }, + tenantId + } }); const organizations = await dataSource.manager.find(Organization, { where: { - tenantId, - }, + tenantId + } }); for await (const organization of organizations) { const { id: organizationId } = organization; - const projects = await dataSource.manager.findBy( - OrganizationProject, - { - tenantId, - organizationId, - } - ); + const projects = await dataSource.manager.findBy(OrganizationProject, { + tenantId, + organizationId + }); if (!projects) { - console.warn( - 'Warning: projects not found, RandomTasks will not be created' - ); + console.warn('Warning: projects not found, RandomTasks will not be created'); continue; } const teams = await dataSource.manager.findBy(OrganizationTeam, { tenantId, - organizationId, + organizationId }); - const tags: ITag[] = await createTags( - dataSource, - labels, - tenant, - organization - ); + const tags: ITag[] = await createTags(dataSource, labels, tenant, organization); const employees = await dataSource.manager.findBy(Employee, { tenantId, - organizationId, + organizationId }); let count = 0; for await (const issue of issues) { const project = faker.helpers.arrayElement(projects); - const maxTaskNumber = await getMaxTaskNumberByProject( - dataSource, - { - tenantId: tenant.id, - organizationId: organization.id, - projectId: project.id, - } - ); + const maxTaskNumber = await getMaxTaskNumberByProject(dataSource, { + tenantId: tenant.id, + organizationId: organization.id, + projectId: project.id + }); const task = new Task(); - task.tags = filter( - tags, - (tag: ITag) => - !!issue.labels.find( - (label: any) => label.name === tag.name - ) - ); + task.tags = filter(tags, (tag: ITag) => !!issue.labels.find((label: any) => label.name === tag.name)); task.title = issue.title; task.description = issue.body; task.status = issue.state; @@ -207,12 +167,7 @@ export const createRandomTask = async ( } }; -export async function createTags( - dataSource: DataSource, - labels, - tenant: ITenant, - organization: IOrganization -) { +export async function createTags(dataSource: DataSource, labels, tenant: ITenant, organization: IOrganization) { if (labels.length === 0) { return []; } @@ -224,7 +179,7 @@ export async function createTags( description: label.description, color: `#${label.color}`, tenant, - organization, + organization }) ); @@ -237,10 +192,7 @@ export async function createTags( * * @param options */ -export async function getMaxTaskNumberByProject( - dataSource: DataSource, - options: IGetTaskOptions -) { +export async function getMaxTaskNumberByProject(dataSource: DataSource, options: IGetTaskOptions) { const { tenantId, organizationId, projectId } = options; /** * GET maximum task number by project diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index 62afaed1677..a8c1ebc6ca9 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -419,7 +419,9 @@ export class TaskService extends TenantAwareCrudService { query.leftJoinAndSelect(`${query.alias}.members`, 'members'); if (organizationTeamId) { - query.leftJoinAndSelect(`${query.alias}.teams`, 'teams', 'teams.id = :organizationTeamId', { organizationTeamId }); + query.leftJoinAndSelect(`${query.alias}.teams`, 'teams', 'teams.id = :organizationTeamId', { + organizationTeamId + }); } else { query.leftJoinAndSelect(`${query.alias}.teams`, 'teams'); } @@ -432,14 +434,17 @@ export class TaskService extends TenantAwareCrudService { const subQuery = qb.subQuery(); subQuery.select(p('"task_employee"."taskId"')).from(p('task_employee'), p('task_employee')); subQuery.andWhere(p('"task_employee"."employeeId" = :employeeId'), { employeeId }); - subQuery.andWhere(p(`"task_employee"."tenantId" = :tenantId`), { tenantId }); return p('"task_members"."taskId" IN ') + subQuery.distinct(true).getQuery(); }); web.orWhere((qb: SelectQueryBuilder) => { const subQuery = qb.subQuery(); subQuery.select(p('"task_team"."taskId"')).from(p('task_team'), p('task_team')); - subQuery.leftJoin('organization_team_employee', 'ote', p('"ote"."organizationTeamId" = "task_team"."organizationTeamId"')); + subQuery.leftJoin( + 'organization_team_employee', + 'ote', + p('"ote"."organizationTeamId" = "task_team"."organizationTeamId"') + ); subQuery.andWhere(p('"ote"."employeeId" = :employeeId'), { employeeId }); subQuery.andWhere(p(`"ote"."tenantId" = :tenantId`), { tenantId }); @@ -455,7 +460,9 @@ export class TaskService extends TenantAwareCrudService { web.andWhere((qb: SelectQueryBuilder) => { const subQuery = qb.subQuery(); subQuery.select(p('"task_team"."taskId"')).from(p('task_team'), p('task_team')); - subQuery.andWhere(p('"task_teams"."organizationTeamId" = :organizationTeamId'), { employeeId }); + subQuery.andWhere(p('"task_teams"."organizationTeamId" = :organizationTeamId'), { + organizationTeamId + }); subQuery.andWhere(p('"task_teams"."tenantId" = :tenantId'), { tenantId }); return p('"task_teams"."taskId" IN ') + subQuery.distinct(true).getQuery(); }); diff --git a/packages/core/src/tenant/tenant-setting/tenant-setting.service.ts b/packages/core/src/tenant/tenant-setting/tenant-setting.service.ts index c6af2c48727..691df12e6b2 100644 --- a/packages/core/src/tenant/tenant-setting/tenant-setting.service.ts +++ b/packages/core/src/tenant/tenant-setting/tenant-setting.service.ts @@ -4,11 +4,11 @@ import { FindManyOptions, In } from 'typeorm'; import { indexBy, keys, object, pluck } from 'underscore'; import { S3Client, CreateBucketCommand, CreateBucketCommandInput, CreateBucketCommandOutput } from '@aws-sdk/client-s3'; import { ITenantSetting, IWasabiFileStorageProviderConfig } from '@gauzy/contracts'; -import { TenantSetting } from './tenant-setting.entity'; import { TenantAwareCrudService } from './../../core/crud'; +import { MultiORMEnum, parseTypeORMFindToMikroOrm } from '../../core/utils'; +import { TenantSetting } from './tenant-setting.entity'; import { TypeOrmTenantSettingRepository } from './repository/type-orm-tenant-setting.repository'; import { MikroOrmTenantSettingRepository } from './repository/mikro-orm-tenant-setting.repository'; -import { MultiORMEnum, parseTypeORMFindToMikroOrm } from 'core/utils'; @Injectable() export class TenantSettingService extends TenantAwareCrudService { diff --git a/packages/core/src/time-off-request/time-off-request.service.ts b/packages/core/src/time-off-request/time-off-request.service.ts index 329180a4a13..38cbba8ae59 100644 --- a/packages/core/src/time-off-request/time-off-request.service.ts +++ b/packages/core/src/time-off-request/time-off-request.service.ts @@ -17,8 +17,8 @@ import { RequestApproval } from '../request-approval/request-approval.entity'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from './../core/context'; import { prepareSQLQuery as p } from './../database/database.helper'; -import { MikroOrmRequestApprovalRepository } from 'request-approval/repository/mikro-orm-request-approval.repository'; -import { TypeOrmRequestApprovalRepository } from 'request-approval/repository/type-orm-request-approval.repository'; +import { MikroOrmRequestApprovalRepository } from '../request-approval/repository/mikro-orm-request-approval.repository'; +import { TypeOrmRequestApprovalRepository } from '../request-approval/repository/type-orm-request-approval.repository'; import { MikroOrmTimeOffRequestRepository } from './repository/mikro-orm-time-off-request.repository'; import { TypeOrmTimeOffRequestRepository } from './repository/type-orm-time-off-request.repository'; diff --git a/packages/core/src/user-organization/commands/handlers/user-organization.delete.handler.ts b/packages/core/src/user-organization/commands/handlers/user-organization.delete.handler.ts index eeb292a856d..b72cf4527e4 100644 --- a/packages/core/src/user-organization/commands/handlers/user-organization.delete.handler.ts +++ b/packages/core/src/user-organization/commands/handlers/user-organization.delete.handler.ts @@ -1,13 +1,13 @@ +import { UnauthorizedException, BadRequestException } from '@nestjs/common'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { DeleteResult } from 'typeorm'; +import { RolesEnum, ID, IRole } from '@gauzy/contracts'; +import { RequestContext } from '../../../core/context'; import { UserOrganizationDeleteCommand } from '../user-organization.delete.command'; import { UserOrganization } from '../../user-organization.entity'; import { UserService } from '../../../user/user.service'; import { UserOrganizationService } from '../../user-organization.services'; -import { DeleteResult } from 'typeorm'; import { RoleService } from '../../../role/role.service'; -import { RolesEnum, LanguagesEnum, IUser } from '@gauzy/contracts'; -import { UnauthorizedException, BadRequestException } from '@nestjs/common'; -import { I18nService } from 'nestjs-i18n'; /** * 1. Remove user from given organization if user belongs to multiple organizations @@ -17,95 +17,90 @@ import { I18nService } from 'nestjs-i18n'; * 5. Super Admin user can be deleted only by a Super Admin user. */ @CommandHandler(UserOrganizationDeleteCommand) -export class UserOrganizationDeleteHandler - implements ICommandHandler { +export class UserOrganizationDeleteHandler implements ICommandHandler { constructor( - private readonly userOrganizationService: UserOrganizationService, - private readonly userService: UserService, - private readonly roleService: RoleService, - private readonly i18n: I18nService + private readonly _userOrganizationService: UserOrganizationService, + private readonly _userService: UserService, + private readonly _roleService: RoleService ) {} - public async execute( - command: UserOrganizationDeleteCommand - ): Promise { - const { input } = command; + /** + * Executes a command to delete a user organization association. + * + * @param command The delete command containing input data. + * @returns A promise resolving to either the deleted UserOrganization or DeleteResult. + */ + public async execute(command: UserOrganizationDeleteCommand): Promise { + const { userOrganizationId } = command; - // 1. find user to delete + // 1. Find user and their role to determine deletion handling const { user: { role: { name: roleName } }, userId - } = await this.userOrganizationService.findOneByIdString( - input.userOrganizationId, - { relations: ['user', 'user.role'] } - ); + } = await this._userOrganizationService.findOneByIdString(userOrganizationId, { + relations: { user: { role: true } } + }); // 2. Handle Super Admin Deletion if applicable - if (roleName === RolesEnum.SUPER_ADMIN) - return this._removeSuperAdmin( - input.requestingUser, - userId, - input.language - ); + if (roleName === RolesEnum.SUPER_ADMIN) { + return await this._removeSuperAdmin(userId); + } - return this._removeUserFromOrganization( - userId, - input.userOrganizationId - ); + // 3. Remove user from organization based on the number of organizations they belong to + return await this._removeUserFromOrganization(userId, userOrganizationId); } + /** + * Remove user from organization based on the number of organizations they belong to. + * + * @param userId The ID of the user to remove. + * @param userOrganizationId The ID of the user organization association to remove. + * @returns A promise resolving to either the deleted UserOrganization or DeleteResult. + */ private async _removeUserFromOrganization( - userId: string, - userOrganizationId: string + userId: ID, + userOrganizationId: ID ): Promise { - // 1. get count of organizations the user belongs to - const { total } = await this.userOrganizationService.findAll({ - where: { userId } - }); + // 1. Get count of organizations the user belongs to + const total = await this._userOrganizationService.countBy({ userId }); - return total === 1 - ? this.userService.delete(userId) - : this.userOrganizationService.delete(userOrganizationId); + // Decide whether to delete user or user organization based on the count + if (total === 1) { + return await this._userService.delete(userId); // Delete the user if they belong to only one organization + } + return await this._userOrganizationService.delete(userOrganizationId); // Delete the user organization association if they belong to multiple organizations } - private async _removeSuperAdmin( - requestingUser: IUser, - userId: string, - language: LanguagesEnum - ): Promise { + /** + * Remove a Super Admin user from the system. + * + * @param id The ID of the Super Admin user to be removed. + * @returns A promise resolving to either the deleted UserOrganization or DeleteResult. + */ + private async _removeSuperAdmin(id: ID): Promise { + const currentRoleId = RequestContext.currentRoleId(); + const currentTenantId = RequestContext.currentTenantId(); + // 1. Check if the requesting user has permission to delete Super Admin - const { name: requestingUserRoleName } = await this.roleService.findOneByIdString( - requestingUser.roleId - ); + const role: IRole = await this._roleService.findOneByIdString(currentRoleId); - if (requestingUserRoleName !== RolesEnum.SUPER_ADMIN) - throw new UnauthorizedException( - 'Only Super Admin user can delete Super Admin users' - ); + if (role.name !== RolesEnum.SUPER_ADMIN) { + throw new UnauthorizedException('Only Super Admin users can delete Super Admin users'); + } // 2. Check if there are at least 2 Super Admins before deleting Super Admin user - const { total } = await this.userService.findAll({ - where: { - role: { id: requestingUser.roleId }, - tenant: { id: requestingUser.tenantId } - }, - relations: ['role', 'tenant'] + const total = await this._userService.countBy({ + role: { id: currentRoleId }, + tenant: { id: currentTenantId } }); - if (total === 1) - throw new BadRequestException( - await this.i18n.translate( - 'USER_ORGANIZATION.CANNOT_DELETE_ALL_SUPER_ADMINS', - { - lang: language, - args: { count: 1 } - } - ) - ); + if (total === 1) { + throw new BadRequestException(`There must be at least ${total} Super Admin per Tenant`); + } // 3. Delete Super Admin user from all organizations - return this.userService.delete(userId); + return await this._userService.delete(id); } } diff --git a/packages/core/src/user-organization/commands/user-organization.delete.command.ts b/packages/core/src/user-organization/commands/user-organization.delete.command.ts index 18582b5b9f8..265be9a9b0b 100644 --- a/packages/core/src/user-organization/commands/user-organization.delete.command.ts +++ b/packages/core/src/user-organization/commands/user-organization.delete.command.ts @@ -1,8 +1,8 @@ import { ICommand } from '@nestjs/cqrs'; -import { IUserOrganizationDeleteInput } from '@gauzy/contracts'; +import { ID } from '@gauzy/contracts'; export class UserOrganizationDeleteCommand implements ICommand { static readonly type = '[UserOrganization] Delete'; - constructor(public readonly input: IUserOrganizationDeleteInput) {} + constructor(public readonly userOrganizationId: ID) {} } diff --git a/packages/core/src/user-organization/user-organization.controller.ts b/packages/core/src/user-organization/user-organization.controller.ts index f3cb693f850..10fd012a1e0 100644 --- a/packages/core/src/user-organization/user-organization.controller.ts +++ b/packages/core/src/user-organization/user-organization.controller.ts @@ -1,13 +1,10 @@ import { Controller, HttpStatus, Get, Query, UseGuards, HttpCode, Delete, Param } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { Not } from 'typeorm'; -import { I18nLang } from 'nestjs-i18n'; -import { IUserOrganization, RolesEnum, LanguagesEnum, IPagination, IUser } from '@gauzy/contracts'; +import { IUserOrganization, IPagination, ID } from '@gauzy/contracts'; import { CrudController, PaginationParams } from './../core/crud'; import { UUIDValidationPipe } from './../shared/pipes'; import { TenantPermissionGuard } from './../shared/guards'; -import { UserDecorator } from './../shared/decorators'; import { UserOrganizationService } from './user-organization.services'; import { UserOrganization } from './user-organization.entity'; import { UserOrganizationDeleteCommand } from './commands'; @@ -25,15 +22,16 @@ export class UserOrganizationController extends CrudController } /** + * Find all UserOrganizations. * - * @param params - * @returns + * @param params - The pagination parameters. + * @param query - Additional query parameters to filter results. + * @returns A paginated list of UserOrganizations. */ @ApiOperation({ summary: 'Find all UserOrganizations.' }) @ApiResponse({ status: HttpStatus.OK, - description: 'Found UserOrganizations', - type: UserOrganization + description: 'Found UserOrganizations' }) @ApiResponse({ status: HttpStatus.NOT_FOUND, @@ -42,14 +40,19 @@ export class UserOrganizationController extends CrudController @Get() async findAll( @Query() params: PaginationParams, - @Query() query: FindMeUserOrganizationDTO, + @Query() query: FindMeUserOrganizationDTO ): Promise> { - return await this.userOrganizationService.findAllUserOrganizations( - params, - query.includeEmployee - ); + return await this.userOrganizationService.findUserOrganizations(params, query.includeEmployee); } + /** + * Delete user from organization. + * + * @param id - The ID of the user organization to delete. + * @param user - The user making the request. + * @param language - The language to use for any error messages or responses. + * @returns The deleted user organization. + */ @ApiOperation({ summary: 'Delete user from organization' }) @ApiResponse({ status: HttpStatus.NO_CONTENT, @@ -61,20 +64,16 @@ export class UserOrganizationController extends CrudController }) @HttpCode(HttpStatus.ACCEPTED) @Delete(':id') - async delete( - @Param('id', UUIDValidationPipe) id: string, - @UserDecorator() user: IUser, - @I18nLang() language: LanguagesEnum - ): Promise { - return this.commandBus.execute( - new UserOrganizationDeleteCommand({ - userOrganizationId: id, - requestingUser: user, - language - }) - ); + async delete(@Param('id', UUIDValidationPipe) id: ID): Promise { + return await this.commandBus.execute(new UserOrganizationDeleteCommand(id)); } + /** + * Find the number of organizations a user belongs to. + * + * @param id - The user ID. + * @returns The count of organizations the user belongs to. + */ @ApiOperation({ summary: 'Find number of Organizations user belongs to' }) @ApiResponse({ status: HttpStatus.OK, @@ -85,19 +84,29 @@ export class UserOrganizationController extends CrudController status: HttpStatus.NOT_FOUND, description: 'Record not found' }) - @Get(':id') - async findOrganizationCount(@Param('id', UUIDValidationPipe) id: string): Promise { - const { userId } = await this.findById(id); - const { total } = await this.userOrganizationService.findAll({ - where: { - userId, - isActive: true, - user: { - role: { name: Not(RolesEnum.EMPLOYEE) } + @Get(':id/count') + async findOrganizationCount(@Param('id', UUIDValidationPipe) id: ID): Promise { + try { + // Retrieve the user organization by ID + const user = await this.userOrganizationService.findOneByIdString(id); + + // Extract user ID from the retrieved user organization + const { userId } = user; + + // Attempt to count the user organizations + const total = await this.userOrganizationService.count({ + where: { + userId, + isActive: true, + isArchived: false } - }, - relations: ['user', 'user.role'] - }); - return total; + }); + + // Return the total count of user organizations + return total; + } catch (error) { + console.error('Error retrieving user organization count:', error.message); + throw new Error('Failed to retrieve user organization count.'); + } } } diff --git a/packages/core/src/user-organization/user-organization.entity.ts b/packages/core/src/user-organization/user-organization.entity.ts index 6d33c5d3381..8419926de6e 100644 --- a/packages/core/src/user-organization/user-organization.entity.ts +++ b/packages/core/src/user-organization/user-organization.entity.ts @@ -1,23 +1,18 @@ -import { - JoinColumn, - RelationId -} from 'typeorm'; import { ApiProperty } from '@nestjs/swagger'; +import { JoinColumn, RelationId } from 'typeorm'; import { IsUUID } from 'class-validator'; -import { IUser, IUserOrganization } from '@gauzy/contracts'; +import { ID, IUser, IUserOrganization } from '@gauzy/contracts'; import { TenantOrganizationBaseEntity, User } from '../core/entities/internal'; import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from './../core/decorators/entity'; import { MikroOrmUserOrganizationRepository } from './repository/mikro-orm-user-organization.repository'; @MultiORMEntity('user_organization', { mikroOrmRepository: () => MikroOrmUserOrganizationRepository }) export class UserOrganization extends TenantOrganizationBaseEntity implements IUserOrganization { - @ApiProperty({ type: () => Boolean, default: true }) @ColumnIndex() @MultiORMColumn({ default: true }) isDefault: boolean; - /* |-------------------------------------------------------------------------- | @ManyToOne @@ -38,5 +33,5 @@ export class UserOrganization extends TenantOrganizationBaseEntity implements IU @IsUUID() @ColumnIndex() @MultiORMColumn({ relationId: true }) - userId: IUser['id']; + userId?: ID; } diff --git a/packages/core/src/user-organization/user-organization.services.ts b/packages/core/src/user-organization/user-organization.services.ts index 4341fc1c13d..be33733d1b3 100644 --- a/packages/core/src/user-organization/user-organization.services.ts +++ b/packages/core/src/user-organization/user-organization.services.ts @@ -2,19 +2,20 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IOrganization, IPagination, ITenant, IUser, IUserOrganization, RolesEnum } from '@gauzy/contracts'; import { PaginationParams, TenantAwareCrudService } from './../core/crud'; -import { Employee, Organization } from './../core/entities/internal'; +import { Employee } from './../core/entities/internal'; +import { EmployeeService } from '../employee/employee.service'; import { TypeOrmOrganizationRepository } from '../organization/repository'; import { UserOrganization } from './user-organization.entity'; import { MikroOrmUserOrganizationRepository, TypeOrmUserOrganizationRepository } from './repository'; -import { EmployeeService } from '../employee/employee.service'; @Injectable() export class UserOrganizationService extends TenantAwareCrudService { constructor( - @InjectRepository(UserOrganization) readonly typeOrmUserOrganizationRepository: TypeOrmUserOrganizationRepository, + @InjectRepository(UserOrganization) + readonly typeOrmUserOrganizationRepository: TypeOrmUserOrganizationRepository, readonly mikroOrmUserOrganizationRepository: MikroOrmUserOrganizationRepository, - @InjectRepository(Organization) readonly typeOrmOrganizationRepository: TypeOrmOrganizationRepository, - private readonly employeeService: EmployeeService, + readonly typeOrmOrganizationRepository: TypeOrmOrganizationRepository, + readonly employeeService: EmployeeService ) { super(typeOrmUserOrganizationRepository, mikroOrmUserOrganizationRepository); } @@ -25,30 +26,32 @@ export class UserOrganizationService extends TenantAwareCrudService, includeEmployee: boolean ): Promise> { // Call the base class method to find all user organizations - const { items, total } = await super.findAll(filter); + let { items, total } = await super.findAll(filter); // If 'includeEmployee' is set to true, fetch employee details associated with each user organization if (includeEmployee) { try { // Extract user IDs from the items array - const userIds = items.map((organization: IUserOrganization) => organization.user.id); + const userIds = items + .filter((organization: IUserOrganization) => organization.user) // Filter out user organizations without a user object + .map((organization: IUserOrganization) => organization.user.id); // Fetch all employee details in bulk for the extracted user IDs const employees = await this.employeeService.findEmployeesByUserIds(userIds); // Map employee details to a dictionary for easier lookup const employeeMap = new Map(); - employees.forEach((employee) => { + employees.forEach((employee: Employee) => { employeeMap.set(employee.userId, employee); }); // Merge employee details into each user organization object - const itemsWithEmployees = items.map(organization => { + const itemsWithEmployees = items.map((organization: UserOrganization) => { const employee = employeeMap.get(organization.user.id); return { ...organization, user: { ...organization.user, employee } }; }); diff --git a/packages/core/src/user/commands/handlers/user.create.handler.ts b/packages/core/src/user/commands/handlers/user.create.handler.ts index 7bd788b049d..71b260263e2 100644 --- a/packages/core/src/user/commands/handlers/user.create.handler.ts +++ b/packages/core/src/user/commands/handlers/user.create.handler.ts @@ -5,11 +5,14 @@ import { UserService } from '../../user.service'; @CommandHandler(UserCreateCommand) export class UserCreateHandler implements ICommandHandler { + constructor(private readonly userService: UserService) {} - constructor( - private readonly userService: UserService - ) {} - + /** + * Executes the user creation command by calling the UserService to create a new user. + * + * @param command The UserCreateCommand containing user creation input. + * @returns A Promise resolving to the created IUser object. + */ public async execute(command: UserCreateCommand): Promise { const { input } = command; return await this.userService.create(input); diff --git a/packages/core/src/user/commands/user.create.command.ts b/packages/core/src/user/commands/user.create.command.ts index 570a2f5b529..b3fa878fd15 100644 --- a/packages/core/src/user/commands/user.create.command.ts +++ b/packages/core/src/user/commands/user.create.command.ts @@ -4,7 +4,5 @@ import { IUserCreateInput } from '@gauzy/contracts'; export class UserCreateCommand implements ICommand { static readonly type = '[User] Create'; - constructor( - public readonly input: IUserCreateInput - ) {} + constructor(public readonly input: IUserCreateInput) {} } diff --git a/packages/core/src/user/factory-reset/factory-reset.service.ts b/packages/core/src/user/factory-reset/factory-reset.service.ts index dc903e6a83d..6b3b5c98c10 100644 --- a/packages/core/src/user/factory-reset/factory-reset.service.ts +++ b/packages/core/src/user/factory-reset/factory-reset.service.ts @@ -2,12 +2,11 @@ // MIT License, see https://github.com/xmlking/ngx-starter-kit/blob/develop/LICENSE // Copyright (c) 2018 Sumanth Chinthagunta -import { ConfigService } from '@gauzy/config'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { RequestContext } from 'core'; import { Repository, In } from 'typeorm'; import { filter, map, some } from 'underscore'; +import { ConfigService } from '@gauzy/config'; import { Activity, AppointmentEmployee, @@ -95,6 +94,7 @@ import { User, UserOrganization } from '../../core/entities/internal'; +import { RequestContext } from '../../core/context'; import { TypeOrmActivityRepository } from '../../time-tracking/activity/repository/type-orm-activity.repository'; import { MikroOrmActivityRepository } from '../../time-tracking/activity/repository/mikro-orm-activity.repository'; import { MikroOrmAppointmentEmployeeRepository } from '../../appointment-employees/repository/mikro-orm-appointment-employee.repository'; @@ -697,7 +697,7 @@ export class FactoryResetService { mikroOrmUserOrganizationRepository: MikroOrmUserOrganizationRepository, private configService: ConfigService - ) { } + ) {} async onModuleInit() { this.registerCoreRepositories(); diff --git a/packages/core/src/user/user.service.ts b/packages/core/src/user/user.service.ts index ce44c93622a..4d1eb0f2082 100644 --- a/packages/core/src/user/user.service.ts +++ b/packages/core/src/user/user.service.ts @@ -2,12 +2,7 @@ // MIT License, see https://github.com/xmlking/ngx-starter-kit/blob/develop/LICENSE // Copyright (c) 2018 Sumanth Chinthagunta -import { - ForbiddenException, - Injectable, - NotFoundException, - UnauthorizedException -} from '@nestjs/common'; +import { ForbiddenException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { InsertResult, SelectQueryBuilder, @@ -19,7 +14,15 @@ import { } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { JwtPayload } from 'jsonwebtoken'; -import { ComponentLayoutStyleEnum, IEmployee, IFindMeUser, IUser, LanguagesEnum, PermissionsEnum, RolesEnum } from '@gauzy/contracts'; +import { + ComponentLayoutStyleEnum, + IEmployee, + IFindMeUser, + IUser, + LanguagesEnum, + PermissionsEnum, + RolesEnum +} from '@gauzy/contracts'; import { isNotEmpty } from '@gauzy/common'; import { ConfigService, environment as env } from '@gauzy/config'; import { prepareSQLQuery as p } from './../database/database.helper'; @@ -111,19 +114,25 @@ export class UserService extends TenantAwareCrudService { public async markEmailAsVerified(id: IUser['id']) { switch (this.ormType) { case MultiORMEnum.MikroORM: - return await this.mikroOrmRepository.nativeUpdate({ id }, { - emailVerifiedAt: freshTimestamp(), - emailToken: null, - code: null, - codeExpireAt: null - }) + return await this.mikroOrmRepository.nativeUpdate( + { id }, + { + emailVerifiedAt: freshTimestamp(), + emailToken: null, + code: null, + codeExpireAt: null + } + ); case MultiORMEnum.TypeORM: - return await this.typeOrmRepository.update({ id }, { - emailVerifiedAt: freshTimestamp(), - emailToken: null, - code: null, - codeExpireAt: null - }); + return await this.typeOrmRepository.update( + { id }, + { + emailVerifiedAt: freshTimestamp(), + emailToken: null, + code: null, + codeExpireAt: null + } + ); default: throw new Error(`Not implemented for ${this.ormType}`); } @@ -275,7 +284,7 @@ export class UserService extends TenantAwareCrudService { id: id as string, tenantId: RequestContext.currentTenantId() }); - } catch { } + } catch {} } catch (error) { throw new ForbiddenException(); } @@ -426,12 +435,19 @@ export class UserService extends TenantAwareCrudService { } /** + * Asynchronously generates a bcrypt hash from the provided password. * - * @param password - * @returns + * @param password The password to hash. + * @returns A promise resolving to the bcrypt hash of the password. */ private async getPasswordHash(password: string): Promise { - return bcrypt.hash(password, env.USER_PASSWORD_BCRYPT_SALT_ROUNDS); + try { + // Generate bcrypt hash using provided password and salt rounds from environment + return await bcrypt.hash(password, env.USER_PASSWORD_BCRYPT_SALT_ROUNDS); + } catch (error) { + // Handle any errors during hashing process + console.error('Error generating password hash:', error); + } } /** @@ -467,7 +483,9 @@ export class UserService extends TenantAwareCrudService { // TODO: Unassign all the task assigned to this user // Best to raise some event and handle it in the subscriber that remove tasks! const employee = await this._employeeService.findOneByUserId(user.id); - if (employee) { await this._taskService.unassignEmployeeFromTeamTasks(employee.id); } + if (employee) { + await this._taskService.unassignEmployeeFromTeamTasks(employee.id); + } return await super.delete(userId); } catch (error) { diff --git a/packages/desktop-ui-lib/package.json b/packages/desktop-ui-lib/package.json index bbe7dab16d7..251afefa310 100644 --- a/packages/desktop-ui-lib/package.json +++ b/packages/desktop-ui-lib/package.json @@ -54,7 +54,7 @@ "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", "angular2-smart-table": "^3.2.0", - "ckeditor4-angular": "^5.1.0", + "ckeditor4-angular": "4.0.1", "electron-log": "^4.4.8", "electron-store": "^8.1.0", "hash-it": "^6.0.0", diff --git a/packages/plugin/src/plugin.helper.ts b/packages/plugin/src/plugin.helper.ts index c3637c3a2f3..4ea1253eef2 100644 --- a/packages/plugin/src/plugin.helper.ts +++ b/packages/plugin/src/plugin.helper.ts @@ -11,17 +11,12 @@ import { PluginLifecycleMethods } from './plugin.interface'; * @param metadataKey The metadata key to retrieve from plugins. * @returns An array of classes obtained from the provided plugins and metadata key. */ -function getClassesFromPlugins( - plugins: Array | DynamicModule>, - metadataKey: string -): Array> { +function getClassesFromPlugins(plugins: Array | DynamicModule>, metadataKey: string): Array> { if (!plugins) { return []; } - return plugins.flatMap((plugin: Type | DynamicModule) => - reflectMetadata(plugin, metadataKey) ?? [] - ); + return plugins.flatMap((plugin: Type | DynamicModule) => reflectMetadata(plugin, metadataKey) ?? []); } /** @@ -29,9 +24,7 @@ function getClassesFromPlugins( * @param plugins An array of plugins containing entity metadata. * @returns An array of entity classes obtained from the provided plugins. */ -export function getEntitiesFromPlugins( - plugins?: Array | DynamicModule> -): Array> { +export function getEntitiesFromPlugins(plugins?: Array | DynamicModule>): Array> { return getClassesFromPlugins(plugins, PLUGIN_METADATA.ENTITIES); } @@ -40,9 +33,7 @@ export function getEntitiesFromPlugins( * @param plugins An array of plugins containing subscriber metadata. * @returns An array of subscriber classes obtained from the provided plugins. */ -export function getSubscribersFromPlugins( - plugins?: Array | DynamicModule> -): Array> { +export function getSubscribersFromPlugins(plugins?: Array | DynamicModule>): Array> { return getClassesFromPlugins(plugins, PLUGIN_METADATA.SUBSCRIBERS); } @@ -56,8 +47,8 @@ export function getPluginExtensions(plugins: Array | DynamicModule>) { return []; } - return plugins.flatMap((plugin: Type | DynamicModule) => - reflectMetadata(plugin, PLUGIN_METADATA.EXTENSIONS) ?? [] + return plugins.flatMap( + (plugin: Type | DynamicModule) => reflectMetadata(plugin, PLUGIN_METADATA.EXTENSIONS) ?? [] ); } @@ -71,8 +62,8 @@ export function getPluginConfigurations(plugins: (Type | DynamicModule)[] = return []; } - return plugins.flatMap((plugin: Type | DynamicModule) => - reflectMetadata(plugin, PLUGIN_METADATA.CONFIGURATION) || [] + return plugins.flatMap( + (plugin: Type | DynamicModule) => reflectMetadata(plugin, PLUGIN_METADATA.CONFIGURATION) || [] ); } @@ -81,9 +72,7 @@ export function getPluginConfigurations(plugins: (Type | DynamicModule)[] = * @param plugins An array of plugins. * @returns An array of modules obtained from the provided plugins. */ -export function getPluginModules( - plugins: Array | DynamicModule> -): Array> { +export function getPluginModules(plugins: Array | DynamicModule>): Array> { return plugins.map((plugin: Type | DynamicModule) => { if (isDynamicModule(plugin)) { const { module } = plugin; @@ -99,10 +88,7 @@ export function getPluginModules( * @param metadataKey The key for the metadata to be reflected. * @returns The metadata associated with the given key. */ -function reflectMetadata( - metatype: Type | DynamicModule, - metadataKey: string -) { +function reflectMetadata(metatype: Type | DynamicModule, metadataKey: string) { // Extract the module property if the metatype is a DynamicModule const target = isDynamicModule(metatype) ? metatype.module : metatype; @@ -128,9 +114,7 @@ export function hasLifecycleMethod( * @param type The type to check. * @returns True if the type is a DynamicModule, false otherwise. */ -export function isDynamicModule( - type: Type | DynamicModule -): type is DynamicModule { +export function isDynamicModule(type: Type | DynamicModule): type is DynamicModule { return !!(type as DynamicModule).module; } @@ -145,7 +129,7 @@ export function reflectDynamicModuleMetadata(module: Type) { controllers: reflectMetadata(module, MODULE_METADATA.CONTROLLERS) || [], providers: reflectMetadata(module, MODULE_METADATA.PROVIDERS) || [], imports: reflectMetadata(module, MODULE_METADATA.IMPORTS) || [], - exports: reflectMetadata(module, MODULE_METADATA.EXPORTS) || [], + exports: reflectMetadata(module, MODULE_METADATA.EXPORTS) || [] }; } @@ -156,14 +140,16 @@ export function reflectDynamicModuleMetadata(module: Type) { export function getDynamicPluginsModules(): DynamicModule[] { const plugins = getConfig().plugins; - return plugins.map((plugin: Type | DynamicModule) => { - const pluginModule = isDynamicModule(plugin) ? plugin.module : plugin; - const { imports, providers, exports } = reflectDynamicModuleMetadata(pluginModule); - return { - module: pluginModule, - imports, - exports, - providers: [...providers] - }; - }).filter(isNotEmpty); + return plugins + .map((plugin: Type | DynamicModule) => { + const pluginModule = isDynamicModule(plugin) ? plugin.module : plugin; + const { imports, providers, exports } = reflectDynamicModuleMetadata(pluginModule); + return { + module: pluginModule, + imports, + exports, + providers: [...providers] + }; + }) + .filter(isNotEmpty); } diff --git a/packages/plugins/changelog/package.json b/packages/plugins/changelog/package.json index 394bde5298d..50ce0459e85 100644 --- a/packages/plugins/changelog/package.json +++ b/packages/plugins/changelog/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/changelog-plugin", + "name": "@gauzy/plugin-changelog", "version": "0.1.0", "description": "Ever Gauzy Platform Change Log plugin", "author": { diff --git a/packages/plugins/integration-ai/.dockerignore b/packages/plugins/integration-ai/.dockerignore index 3438721757c..14ea7e1f315 100644 --- a/packages/plugins/integration-ai/.dockerignore +++ b/packages/plugins/integration-ai/.dockerignore @@ -1,10 +1,23 @@ +# Ignore Git related files and directories .git .gitignore .gitmodules + +# Ignore README file README.md + +# Ignore Docker-related files and directories docker + +# Ignore Node.js modules node_modules + +# Ignore temporary files and directories tmp + +# Ignore build and compilation output build dist + +# Ignore environment configuration files .env diff --git a/packages/plugins/integration-ai/.eslintrc.json b/packages/plugins/integration-ai/.eslintrc.json new file mode 100644 index 00000000000..79fd7c1d982 --- /dev/null +++ b/packages/plugins/integration-ai/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/plugins/integration-ai/.gitignore b/packages/plugins/integration-ai/.gitignore new file mode 100644 index 00000000000..fe30e94bebf --- /dev/null +++ b/packages/plugins/integration-ai/.gitignore @@ -0,0 +1,6 @@ +# dependencies +node_modules/ + +# misc +npm-debug.log +dist diff --git a/packages/plugins/integration-ai/.npmignore b/packages/plugins/integration-ai/.npmignore new file mode 100644 index 00000000000..1eb4beb9572 --- /dev/null +++ b/packages/plugins/integration-ai/.npmignore @@ -0,0 +1,4 @@ +# .npmignore + +src/ +node_modules/ diff --git a/packages/plugins/integration-ai/README.md b/packages/plugins/integration-ai/README.md index 1ef16f7968c..bf84ea9f1c7 100644 --- a/packages/plugins/integration-ai/README.md +++ b/packages/plugins/integration-ai/README.md @@ -1,7 +1,15 @@ # Integration Gauzy AI +This library was generated with [Nx](https://nx.dev). + +# Description + This library provides integration with Gauzy AI project. +## Building + +Run `yarn run build` to build the library. + ## Running unit tests Run `ng test integration-ai` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/plugins/integration-ai/jest.config.js b/packages/plugins/integration-ai/jest.config.js deleted file mode 100644 index acd49f27132..00000000000 --- a/packages/plugins/integration-ai/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - name: 'integration-ai', - preset: '../../../jest.config.js', - transform: { - '^.+\\.[tj]sx?$': 'ts-jest' - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], - coverageDirectory: '../../coverage/plugins/integration-ai' -}; diff --git a/packages/plugins/integration-ai/jest.config.ts b/packages/plugins/integration-ai/jest.config.ts new file mode 100644 index 00000000000..8bebe9b7556 --- /dev/null +++ b/packages/plugins/integration-ai/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'integration-ai', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }] + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/packages/plugins/integration-ai' +}; diff --git a/packages/plugins/integration-ai/package.json b/packages/plugins/integration-ai/package.json index 8a1ce613823..cd48284a03b 100644 --- a/packages/plugins/integration-ai/package.json +++ b/packages/plugins/integration-ai/package.json @@ -1,13 +1,32 @@ { "name": "@gauzy/integration-ai", "version": "0.1.0", - "description": "Ever Gauzy Platform plugin for integration with Gauzy AI", + "description": "Enhance Ever Gauzy Platform with advanced AI integration capabilities for streamlined business management and automation.", "author": { "name": "Ever Co. LTD", "email": "ever@ever.co", "url": "https://ever.co" }, "license": "AGPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/ever-co/ever-gauzy" + }, + "bugs": { + "url": "https://github.com/ever-co/ever-gauzy/issues" + }, + "homepage": "https://ever.co", + "keywords": [ + "Ever Gauzy", + "Gauzy AI", + "plugin", + "integration", + "platform", + "management", + "business", + "tool", + "automation" + ], "private": true, "main": "dist/index.js", "types": "dist/index.d.ts", @@ -22,23 +41,30 @@ "access": "restricted" }, "scripts": { - "test:e2e": "jest --config ./jest.config.js", - "build": "rimraf dist && yarn run compile", - "compile": "tsc -p tsconfig.lib.json" + "test:e2e": "jest --config ./jest.config.ts", + "build": "rimraf dist && tsc -p tsconfig.lib.json", + "build:prod": "rimraf dist && tsc -p tsconfig.lib.prod.json" }, - "keywords": [], "dependencies": { "@apollo/client": "^3.6.2", + "@gauzy/contracts": "^0.1.0", "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.3.7", "@nestjs/config": "^3.2.0", "axios": "^1.6.8", "chalk": "4.1.2", - "form-data": "^3.0.0" + "form-data": "^3.0.0", + "rxjs": "^7.4.0" }, "devDependencies": { + "@types/jest": "^29.4.4", "@types/node": "^20.14.9", "rimraf": "^3.0.2", "typescript": "5.1.6" - } + }, + "engines": { + "node": ">=20.11.1", + "yarn": ">=1.22.19" + }, + "sideEffects": false } diff --git a/packages/plugins/integration-ai/project.json b/packages/plugins/integration-ai/project.json new file mode 100644 index 00000000000..c887d516cd0 --- /dev/null +++ b/packages/plugins/integration-ai/project.json @@ -0,0 +1,48 @@ +{ + "name": "integration-ai", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugins/integration-ai/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "./packages/plugins/integration-ai/dist", + "tsConfig": "packages/plugins/integration-ai/tsconfig.lib.json", + "packageJson": "packages/plugins/integration-ai/package.json", + "main": "packages/plugins/integration-ai/src/index.ts", + "assets": ["packages/plugins/integration-ai/*.md"] + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "node tools/scripts/publish.mjs plugins-integration-ai {args.ver} {args.tag}" + }, + "dependsOn": ["build"] + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/plugins/integration-ai/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/plugins/integration-ai/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + }, + "tags": [] +} diff --git a/packages/plugins/integration-ai/src/config/gauzy-ai.ts b/packages/plugins/integration-ai/src/config/gauzy-ai.ts deleted file mode 100644 index e981c295904..00000000000 --- a/packages/plugins/integration-ai/src/config/gauzy-ai.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { registerAs } from '@nestjs/config'; - -/** - * Gauzy AI Configuration - */ -export default registerAs('guazyAI', () => ({ - // GraphQL endpoint for Gauzy AI - gauzyAIGraphQLEndpoint: process.env.GAUZY_AI_GRAPHQL_ENDPOINT || null, - // REST endpoint for Gauzy AI - gauzyAIRESTEndpoint: process.env.GAUZY_AI_REST_ENDPOINT || null, - // Request timeout for Gauzy AI in milliseconds - gauzyAIRequestTimeout: parseInt(process.env.GAUZY_AI_REQUEST_TIMEOUT) || 60 * 5 * 1000, - // Gauzy AI API keys Pair - gauzyAiApiKey: process.env.GAUZY_AI_API_KEY || null, - gauzyAiApiSecret: process.env.GAUZY_AI_API_SECRET || null -})); diff --git a/packages/plugins/integration-ai/src/gauzy-ai.service.ts b/packages/plugins/integration-ai/src/gauzy-ai.service.ts deleted file mode 100644 index 7ea4565826d..00000000000 --- a/packages/plugins/integration-ai/src/gauzy-ai.service.ts +++ /dev/null @@ -1,1857 +0,0 @@ -import { BadRequestException, HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { HttpService } from '@nestjs/axios'; -import { AxiosRequestConfig, AxiosResponse } from 'axios'; -import qs from 'qs'; -import { Observable, firstValueFrom } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import { - CreateEmployeeJobApplication, - User, - Employee, - EmployeeJobPostsDocument, - EmployeeJobPostsQuery, - EmployeeQuery, - UpdateEmployee, - UpdateEmployeeJobPost, - UpworkJobsSearchCriterion, - UserConnection, - Query, - TenantConnection, - UpdateTenantApiKey, - TenantApiKeyConnection, - Tenant, - EmployeeJobPostFilter -} from './sdk/gauzy-ai-sdk'; -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; -import fetch from 'cross-fetch'; -import * as chalk from 'chalk'; -import * as FormData from 'form-data'; -import { - ApolloClient, - ApolloQueryResult, - NormalizedCacheObject, - InMemoryCache, - DefaultOptions, - // NetworkStatus, - gql, - createHttpLink, - ApolloLink, -} from '@apollo/client/core'; -import { - IEmployeeUpworkJobsSearchCriterion, - IEmployee, - IVisibilityJobPostInput, - IEmployeeJobApplication, - IUpdateEmployeeJobPostAppliedResult, - IEmployeeJobApplicationAppliedResult, - IGetEmployeeJobPostInput, - IPagination, - IEmployeeJobPost, - IJobPost, - IGetEmployeeJobPostFilters, - JobPostStatusEnum, - JobPostTypeEnum, - IEmployeeJobsStatistics, -} from '@gauzy/contracts'; -import { RequestConfigProvider } from './request-config.provider'; -import { AxiosRequestHeaders, HttpMethodEnum } from './configuration.interface'; - -export interface ImageAnalysisResult { - success: boolean; - data: { - mimetype: string; - filename: string; - analysis: Array<{ - work: boolean; - description: string; - apps: string[]; - }>; - message?: string; - }; -} - -@Injectable() -export class GauzyAIService { - private readonly _logger = new Logger(GauzyAIService.name); - private _client: ApolloClient; - public logging: boolean = false; - - // For now, we disable Apollo client caching for all GraphQL queries and mutations - private readonly defaultOptions: DefaultOptions = { - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'ignore', - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all', - }, - mutate: { - fetchPolicy: 'no-cache', - errorPolicy: 'all', - }, - }; - - private gauzyAIGraphQLEndpoint: string; - - constructor( - private readonly _configService: ConfigService, - private readonly _http: HttpService, - private readonly _requestConfigProvider: RequestConfigProvider, - ) { - this.init(); - } - - /** - * Send an HTTP request with dynamic configuration. - * - * @param path The URL path for the request. - * @param options Custom Axios request configuration. - * @param method The HTTP method (e.g., GET, POST). - * @returns An Observable that emits the response data or throws an error. - */ - private sendRequest( - path: string, - options: AxiosRequestConfig = {}, - method: string = HttpMethodEnum.GET, - defaultHeaders: AxiosRequestHeaders = { - 'Content-Type': 'application/json', // Define default headers - } - ): Observable> { - /** */ - const { apiKey, apiSecret, openAiSecretKey, openAiOrganizationId, bearerTokenApi, tenantIdApi } = this._requestConfigProvider.getConfig(); - - // Add your custom headers - const customHeaders = (): AxiosRequestHeaders => ({ - // Define default headers - ...defaultHeaders, - // Add your custom headers here - 'X-APP-ID': this._configService.get('guazyAI.gauzyAiApiKey'), - 'X-API-KEY': this._configService.get('guazyAI.gauzyAiApiSecret'), - - /** */ - ...(apiKey ? { 'X-APP-ID': apiKey } : {}), - ...(apiSecret ? { 'X-API-KEY': apiSecret } : {}), - ...(openAiSecretKey ? { 'X-OPENAI-SECRET-KEY': openAiSecretKey } : {}), - ...(openAiOrganizationId ? { 'X-OPENAI-ORGANIZATION-ID': openAiOrganizationId } : {}), - - /** */ - ...(bearerTokenApi ? { 'Authorization': bearerTokenApi } : {}), - ...(tenantIdApi ? { 'Tenant-Id': tenantIdApi } : {}), - }); - - /** */ - const headers: AxiosRequestHeaders = customHeaders(); - - if (this.logging) { - console.log('Default AxiosRequestConfig Headers: %s', `${JSON.stringify(headers)}`); - } - - // Merge the provided options with the default options - const mergedOptions: AxiosRequestConfig = { - ...options, - // Inside your sendRequest method, use qs.stringify for custom parameter serialization - paramsSerializer: (params) => { - // console.log('Customize the serialization of URL parameters', params); - if (Object.keys(params).length > 0) { - // Customize the serialization of URL parameters as needed - return qs.stringify(params, { arrayFormat: 'repeat' }); - } - } - }; - // console.log('Default AxiosRequestConfig Options: %s', `${JSON.stringify(mergedOptions)}`); - - try { - return this._http.request({ - ...mergedOptions, - url: path, - method, - headers, - }); - } catch (error) { - console.log('Error while sending an HTTP request with dynamic configuration.: %s', error); - if (error.response) { - // Handle HTTP error responses - throw new HttpException(error.response.data, error.response.status); - } else { - // Handle other types of errors (e.g., network issues) - throw new HttpException('An error occurred while making the request.', HttpStatus.INTERNAL_SERVER_ERROR); - } - } - } - - /** - * Analyze an image/screenshot using Gauzy AI. - * - * @param files - Array of Buffers representing the uploaded images. - * @returns Promise - The analysis result for the image. - */ - public async analyzeImage(stream: Buffer, file: any): Promise { - // Create FormData and append the image data - const form = new FormData(); - - // Assuming you have an image file or buffer - form.append(`files`, stream, { - filename: file.filename, - contentType: 'application/octet-stream' - }); - - // Set custom headers - const headers = { - ...form.getHeaders(), - 'Content-Length': form.getLengthSync().toString(), - // Add any other headers you need - }; - - // Set request options - const options = { - data: form, // Set the request payload - params: {} - }; - - // Call the sendRequest function with the appropriate parameters - return await firstValueFrom( - this.sendRequest('image/process', options, HttpMethodEnum.POST, headers).pipe( - map((resp: AxiosResponse) => resp.data) - ) - ); - } - - /** - * Call pre process method to create new employee job application record. - * - * @param params - * @returns - */ - public async preProcessEmployeeJobApplication( - params: IEmployeeJobApplication - ): Promise { - // First we need to get employee id because we have only externalId - params.employeeId = await this.getEmployeeGauzyAIId(params.employeeId); - - // Call the sendRequest function with the appropriate parameters - return await firstValueFrom( - this.sendRequest('employee/job/application/pre-process', { - data: params, // Set the request payload - }, HttpMethodEnum.POST).pipe( - tap((resp: AxiosResponse) => console.log(resp)), - map((resp: AxiosResponse) => resp.data) - ) - ); - } - - /** - * Generate AI proposal for employee job application - * - * @param employeeJobApplicationId - * @returns - */ - public async generateAIProposalForEmployeeJobApplication( - employeeJobApplicationId: string - ): Promise { - // Call the sendRequest function with the appropriate parameters - return await firstValueFrom( - this.sendRequest(`employee/job/application/generate-proposal/${employeeJobApplicationId}`, {}, HttpMethodEnum.POST).pipe( - tap((resp: AxiosResponse) => console.log(resp)), - map((resp: AxiosResponse) => resp.data) - ) - ); - } - - /** - * Get employee job application where proposal generated by AI - * - * @param employeeJobApplicationId - * @returns - */ - public async getEmployeeJobApplication( - employeeJobApplicationId: string - ): Promise { - // Call the sendRequest function with the appropriate parameters - return await firstValueFrom( - this.sendRequest(`employee/job/application/${employeeJobApplicationId}`).pipe( - tap((resp: AxiosResponse) => console.log(resp)), - map((resp: AxiosResponse) => resp.data) - ) - ); - } - - /** - * Get statistic from Gauzy AI about how many jobs are available for given employee - * and to how many of jobs employee already applied and more statistic in the future. - */ - public async getEmployeesStatistics(): Promise { - return []; - } - - /** - * Updates in Gauzy AI if given Employee looking for a jobs or not. - * If not looking, Gauzy AI will NOT return jobs for such employee and will NOT crawl sources for jobs for such employee - * @param employeeId - * @param isJobSearchActive - */ - public async updateEmployeeStatus({ - employeeId, - tenantId, - organizationId, - isJobSearchActive - }: { - employeeId: string, - userId: string, - tenantId: string, - organizationId: string, - isJobSearchActive: boolean - }): Promise { - if (this._client == null) { - return false; - } - - // First we need to get employee id because we have only externalId - const gauzyAIEmployeeId = await this.getEmployeeGauzyAIId(employeeId); - - console.log( - `updateEmployeeStatus called. EmployeeId: ${employeeId}. Gauzy AI EmployeeId: ${gauzyAIEmployeeId}` - ); - - const update: UpdateEmployee = { - externalEmployeeId: employeeId, - externalTenantId: tenantId, - externalOrgId: organizationId, - isActive: isJobSearchActive, - isArchived: !isJobSearchActive, - }; - - const updateEmployeeMutation: DocumentNode = gql` - mutation updateOneEmployee($input: UpdateOneEmployeeInput!) { - updateOneEmployee(input: $input) { - externalEmployeeId - externalTenantId - externalOrgId - isActive - isArchived - } - } - `; - - await this._client.mutate({ - mutation: updateEmployeeMutation, - variables: { - input: { - id: gauzyAIEmployeeId, - update: update, - }, - }, - }); - - return true; - } - - /** - * Apply for a Job - * @param input - * @returns - */ - public async apply( - input: IEmployeeJobApplication - ): Promise { - if (this._client == null) { - return { - ...input, - isRedirectRequired: true, - }; - } - - // First we need to get employee id because we have only externalId - const employeeId = await this.getEmployeeGauzyAIId(input.employeeId); - console.log( - chalk.green(`Method 'apply' is called. EmployeeId: ${employeeId}`) - ); - - // Next we need to get a job using providerCode and providerJobId - const jobPostId = await this.getJobPostId( - input.providerCode, - input.providerJobId - ); - console.log( - chalk.green(`Method 'apply' is called. jobPostId: ${jobPostId}`) - ); - - // Next, we need to find `public employee job post` table record in Gauzy AI to get id of record. - // We can find by employeeId and jobPostId - - const employeeJobPostId = await this.getEmployeeJobPostId( - employeeId, - jobPostId - ); - console.log( - chalk.green( - `Method 'apply' is called. employeeJobPostId: ${employeeJobPostId}` - ) - ); - - if (employeeId && jobPostId && employeeJobPostId) { - const applicationDate = new Date(); - - // ------------------ Create Employee Job Application ------------------ - // This will Apply to the job using Automation system - - const createOneEmployeeJobApplication: CreateEmployeeJobApplication = - { - employeeId: employeeId, - jobPostId: jobPostId, - proposal: input.proposal, - rate: input.rate, - // details: input.details, - attachments: input.attachments, - appliedDate: applicationDate, - employeeJobPostId: employeeJobPostId, - isActive: true, - isArchived: false, - providerCode: input.providerCode, - providerJobId: input.providerJobId, - jobType: input.jobType, - jobStatus: input.jobStatus, - terms: input.terms, - qa: input.qa, - // Note: isViewedByClient will be updated by our Automation system - // Note: providerJobApplicationId will be set by Automation system when it's applied to the job - }; - - // Call the sendRequest function with the appropriate parameters - const response = await firstValueFrom( - this.sendRequest(`employee/job/application/process`, { - method: HttpMethodEnum.POST, // Set the HTTP method to GET - data: createOneEmployeeJobApplication - }).pipe( - map((resp: AxiosResponse) => resp.data) - ) - ); - - return { - ...response, - isRedirectRequired: false, - }; - } else { - return { ...input, isRedirectRequired: true }; - } - } - - /** - * Updates job visibility - * @param hide Should job be hidden or visible. This will set isActive field to false in Gauzy AI - * @param employeeId If employeeId set, job will be set not active only for that specific employee (using EmployeeJobPost record update in Gauzy AI) - * If employeeId is not set, job will be set not active for all employees (using JobPost record update in Gauzy AI) - * @param providerCode e.g. 'upwork' - * @param providerJobId Unique job id in the provider, e.g. in Upwork. If this value is not set, it will update ALL jobs for given provider - */ - public async updateVisibility( - input: IVisibilityJobPostInput - ): Promise { - if (this._client == null) { - return false; - } - - // If it's for specific employee and specific job - if (input.employeeId && input.providerCode && input.providerJobId) { - // First we need to get employee id because we have only externalId - const employeeId = await this.getEmployeeGauzyAIId( - input.employeeId - ); - - console.log(`updateVisibility called. EmployeeId: ${employeeId}`); - - // Next we need to get a job using providerCode and providerJobId - const jobPostId = await this.getJobPostId( - input.providerCode, - input.providerJobId - ); - - console.log(`updateVisibility called. jobPostId: ${jobPostId}`); - - // Next, we need to find `public employee job post` table record in Gauzy AI to get id of record. - // We can find by employeeId and jobPostId - - const employeeJobPostId = await this.getEmployeeJobPostId( - employeeId, - jobPostId - ); - - console.log( - `updateVisibility called. employeeJobPostId: ${employeeJobPostId}` - ); - - if (employeeId && jobPostId && employeeJobPostId) { - const update: UpdateEmployeeJobPost = { - employeeId: employeeId, - jobPostId: jobPostId, - isActive: !input.hide, - isArchived: input.hide, - }; - - const updateEmployeeJobPostMutation: DocumentNode = gql` - mutation updateOneEmployeeJobPost( - $input: UpdateOneEmployeeJobPostInput! - ) { - updateOneEmployeeJobPost(input: $input) { - employeeId - jobPostId - isActive - isArchived - isApplied - appliedDate - } - } - `; - - await this._client.mutate({ - mutation: updateEmployeeJobPostMutation, - variables: { - input: { - id: employeeJobPostId, - update: update, - }, - }, - }); - - return true; - } - } else { - // OK, so it's for all jobs for all employees or for all jobs on specific employee - // TODO: implement - } - - return false; - } - - /** - * Create Employee Job Application and updates Employee Job Post record that employee applied for a job - * NOTE: We will not use this method for now. - * - * Inside interface IEmployeeJobApplication we get below fields - * applied: boolean; <- This will set isApplied and appliedDate fields in Gauzy AI - * employeeId: string; <- Employee who applied for a job - * providerCode: string; <- e.g. 'upwork' - * providerJobId: string; <- Unique job id in the provider, e.g. in Upwork - * proposal?: string; <- Proposal text (optional) - * rate?: number; <- Rate (optional, number) - * details?: string; <- Details (optional) - * attachments?: string; <- Attachments (optional, comma separated list of file names) - */ - public async updateApplied( - input: IEmployeeJobApplication - ): Promise { - if (this._client == null) { - return { isRedirectRequired: true }; - } - - // First we need to get employee id because we have only externalId - const employeeId = await this.getEmployeeGauzyAIId(input.employeeId); - console.log( - chalk.green(`updateApplied called. EmployeeId: ${employeeId}`) - ); - - // Next we need to get a job using providerCode and providerJobId - const jobPostId = await this.getJobPostId( - input.providerCode, - input.providerJobId - ); - console.log( - chalk.green(`updateApplied called. jobPostId: ${jobPostId}`) - ); - - // Next, we need to find `public employee job post` table record in Gauzy AI to get id of record. - // We can find by employeeId and jobPostId - - const employeeJobPostId = await this.getEmployeeJobPostId( - employeeId, - jobPostId - ); - console.log( - chalk.green( - `updateApplied called. employeeJobPostId: ${employeeJobPostId}` - ) - ); - - if (employeeId && jobPostId && employeeJobPostId) { - const applicationDate = new Date(); - - // ------------------ Create Employee Job Application ------------------ - // This will Apply to the job using Automation system - - const createOneEmployeeJobApplication: CreateEmployeeJobApplication = - { - employeeId: employeeId, - jobPostId: jobPostId, - proposal: input.proposal, - rate: input.rate, - // details: input.details, - attachments: input.attachments, - appliedDate: applicationDate, - employeeJobPostId: employeeJobPostId, - isActive: true, - isArchived: false, - providerCode: input.providerCode, - providerJobId: input.providerJobId, - jobType: input.jobType, - jobStatus: input.jobStatus, - terms: input.terms, - qa: input.qa, - // Note: isViewedByClient will be updated by our Automation system - // Note: providerJobApplicationId will be set by Automation system when it's applied to the job - }; - - const createOneEmployeeJobApplicationMutation: DocumentNode = gql` - mutation createOneEmployeeJobApplication( - $input: CreateOneEmployeeJobApplicationInput! - ) { - createOneEmployeeJobApplication(input: $input) { - employeeId - jobPostId - proposal - rate - attachments - appliedDate - employeeJobPostId - isActive - isArchived - providerCode - providerJobId - jobType - jobStatus - terms - qa - } - } - `; - - await this._client.mutate({ - mutation: createOneEmployeeJobApplicationMutation, - variables: { - input: { - employeeJobApplication: createOneEmployeeJobApplication, - }, - }, - }); - - // ------------------ Update Employee Job Post Record ------------------ - // Note: it's just set isApplied and appliedDate fields in Gauzy AI - - const update: UpdateEmployeeJobPost = { - employeeId: employeeId, - jobPostId: jobPostId, - isApplied: input.applied || true, - appliedDate: applicationDate, - }; - - const updateEmployeeJobPostMutation: DocumentNode = gql` - mutation updateOneEmployeeJobPost( - $input: UpdateOneEmployeeJobPostInput! - ) { - updateOneEmployeeJobPost(input: $input) { - employeeId - jobPostId - isActive - isArchived - isApplied - appliedDate - } - } - `; - - await this._client.mutate({ - mutation: updateEmployeeJobPostMutation, - variables: { - input: { - id: employeeJobPostId, - update: update, - }, - }, - }); - } - - // TODO: here we need to check what returned from Gauzy AI - // Because for some providers (e.g. Upwork), redirect to apply manually required - // But for other providers, apply can work inside Gauzy AI automatically - return { isRedirectRequired: true }; - } - - // We call this on each "Save" operation for matching for employee. - // Both when Preset saved for given employee and when any criteria saved for given employee (new criteria or changes in criteria) - // You should pass `employee` entity for which anything on Matching page was changes - // IMPORTANT: You should ALWAYS pass ALL criteria defined for given employee on Matching page, not only new or changed! - // Best way to call this method, is to reload from Gauzy DB all criteria for given employee before call this method. - // We DO NOT USE DATA YOU PASS FROM UI! - // INSTEAD, We CALL THIS METHOD FROM YOUR CQRS COMMAND HANDLERS when you detect that anything related to matching changes - // But as explained above, we must reload criteria from DB, not use anything you have in the local variables - // (because it might not be full data, but this method requires all data to be synced to Gauzy AI, even if such data was previously already synced) - // How this method will work internally: - // - it will call sync for employee first and if no such employee exists in Gauzy AI, it will create new. If exists, it will update employee properties, e.g. lastName - // - next, it will remove all criteria for employee in Gauzy AI and create new records again for criterions. - // I.e. no update will be done, it will be full replacement - // The reason it's acceptable is because such data changes rarely for given employee, so it's totally fine to recreate it - // NOTE: will need to call this method from multiple different CQRS command handlers! - public async syncGauzyEmployeeJobSearchCriteria( - employee: IEmployee, - criteria: IEmployeeUpworkJobsSearchCriterion[] - ): Promise { - if (this._client == null) { - return false; - } - - console.log(`syncGauzyEmployeeJobSearchCriteria called. Criteria: ${JSON.stringify(criteria)}. Employee: ${JSON.stringify(employee)}`); - - try { - const gauzyAIUser: User = await this.syncUser({ - firstName: employee.user.firstName, - lastName: employee.user.lastName, - email: employee.user.email, - username: employee.user.username, - hash: employee.user.hash, - externalTenantId: employee.user.tenantId, - externalUserId: employee.user.id, - isActive: employee.isActive, - isArchived: false - }); - console.log(`Synced User ${JSON.stringify(gauzyAIUser)}`); - /** */ - const gauzyAIEmployee: Employee = await this.syncEmployee({ - externalEmployeeId: employee.id, - externalTenantId: employee.tenantId, - externalOrgId: employee.organizationId, - upworkOrganizationId: employee.organization.upworkOrganizationId, - upworkOrganizationName: employee.organization.upworkOrganizationName, - upworkId: employee.upworkId, - linkedInId: employee.linkedInId, - isActive: employee.isActive, - isArchived: false, - firstName: employee.user.firstName, - lastName: employee.user.lastName, - userId: gauzyAIUser.id - }); - console.log(`Synced Employee ${JSON.stringify(gauzyAIEmployee)}`); - - // let's delete all criteria for Employee - - const deleteAllCriteriaMutation: DocumentNode = gql` - mutation deleteManyUpworkJobsSearchCriteria( - $input: DeleteManyUpworkJobsSearchCriteriaInput! - ) { - deleteManyUpworkJobsSearchCriteria(input: $input) { - deletedCount - } - } - `; - - const deleteMutationResult = await this._client.mutate({ - mutation: deleteAllCriteriaMutation, - variables: { - input: { - filter: { - isActive: { - is: true, - }, - isArchived: { - is: false - }, - employeeId: { - eq: gauzyAIEmployee.id, - } - }, - }, - }, - }); - - console.log( - `Delete Existed Criterions count: ${JSON.stringify( - deleteMutationResult.data.deleteManyUpworkJobsSearchCriteria.deletedCount - )}` - ); - - // now let's create new criteria in Gauzy AI based on Gauzy criterions data - - if (criteria && criteria.length > 0) { - const gauzyAICriteria: UpworkJobsSearchCriterion[] = []; - - criteria.forEach((criterion: IEmployeeUpworkJobsSearchCriterion) => { - gauzyAICriteria.push({ - employeeId: gauzyAIEmployee.id, - isActive: true, - isArchived: false, - jobType: criterion.jobType, - keyword: criterion.keyword, - ...(criterion.category?.name ? { category: criterion.category?.name } : {}), - ...(criterion.categoryId ? { categoryId: criterion.categoryId } : {}), - ...(criterion.occupation?.name ? { occupation: criterion.occupation?.name } : {}), - ...(criterion.occupationId ? { occupationId: criterion.occupationId } : {}), - }); - }); - - const createManyUpworkJobsSearchCriteriaMutation: DocumentNode = gql` - mutation CreateManyUpworkJobsSearchCriteria( - $input: CreateManyUpworkJobsSearchCriteriaInput! - ) { - createManyUpworkJobsSearchCriteria(input: $input) { - id - } - } - `; - - const createNewCriteriaResult = await this._client.mutate({ - mutation: createManyUpworkJobsSearchCriteriaMutation, - variables: { - input: { - upworkJobsSearchCriteria: gauzyAICriteria, - }, - }, - }); - - console.log( - `Create New Criteria result: ${JSON.stringify(createNewCriteriaResult.data.createManyUpworkJobsSearchCriteria)}` - ); - } - - return true; - } catch (error) { - console.log('Error while synced employee: %s', error?.message); - this._logger.error(error); - return false; - } - } - - /** - * Creates employees in Gauzy AI if not exists yet. If exists, updates fields, including externalEmployeeId - * How it works: - * - search done externalEmployeeId field first in Gauzy AI to be equal to Gauzy employee Id. - * - if no record found in Gauzy AI, it search Gauzy AI employees records by employee name - * - if no record found in Gauzy AI, it creates new employee in Gauzy AI - * - * @param employees - */ - public async syncEmployees(employees: IEmployee[]): Promise { - if (this._client == null) { - return false; - } - try { - await Promise.all( - employees.map(async (employee) => { - try { - try { - /** */ - const gauzyAIUser: User = await this.syncUser({ - firstName: employee.user.firstName, - lastName: employee.user.lastName, - email: employee.user.email, - username: employee.user.username, - hash: employee.user.hash, - externalTenantId: employee.user.tenantId, - externalUserId: employee.user.id, - isActive: employee.isActive, - isArchived: !employee.isActive - }); - console.log(`Synced User ${JSON.stringify(gauzyAIUser)}`); - - try { - /** */ - const gauzyAIEmployee: Employee = await this.syncEmployee({ - externalEmployeeId: employee.id, - externalTenantId: employee.tenantId, - externalOrgId: employee.organizationId, - upworkOrganizationId: employee.organization.upworkOrganizationId, - upworkOrganizationName: employee.organization.upworkOrganizationName, - upworkId: employee.upworkId, - linkedInId: employee.linkedInId, - isActive: employee.isActive, - isArchived: !employee.isActive, - firstName: employee.user.firstName, - lastName: employee.user.lastName, - userId: gauzyAIUser.id - }); - console.log(`Synced Employee ${JSON.stringify(gauzyAIEmployee)}`); - } catch (error) { - console.log('Error while syncing employee: %s', error?.message); - this._logger.error(error); - - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - } catch (error) { - console.log('Error while syncing user: %s', error?.message); - this._logger.error(error); - - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - } catch (error) { - // Handle errors for each employee if necessary - console.error(`Error processing sync employee: ${employee.id}`, error?.message); - this._logger.error(error); - - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - }) - ); - return true; - } catch (error) { - console.log('Error while syncing employees: %s', error?.message); - return false; - } - } - - /** - * Get Jobs available for registered employees - */ - public async getEmployeesJobPosts( - data: IGetEmployeeJobPostInput - ): Promise> { - if (this._client == null) { - return null; - } - - const filters: IGetEmployeeJobPostFilters = data.filters ? data.filters : undefined; - console.log(`getEmployeesJobPosts. Filters ${JSON.stringify(filters)}`); - - const employeeIdFilter = filters && filters.employeeIds && filters.employeeIds.length > 0 ? filters.employeeIds[0] : undefined; - try { - // TODO: use Query saved in SDK, not hard-code it here. Note: we may add much more fields to that query as we need more info! - const employeesQuery: DocumentNode = gql` - query employeeJobPosts( - $after: ConnectionCursor! - $first: Int! - $filter: EmployeeJobPostFilter! - $sorting: [EmployeeJobPostSort!] - ) { - employeeJobPosts( - paging: { after: $after, first: $first } - filter: $filter - sorting: $sorting - ) { - totalCount - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - node { - id - isApplied - appliedDate - createdAt - updatedAt - isActive - isArchived - employee { - id - externalEmployeeId - } - providerCode - providerJobId - jobDateCreated - jobStatus - jobType - jobPost { - id - providerCode - providerJobId - title - description - jobDateCreated - jobStatus - jobType - url - budget - duration - workload - skills - category - subcategory - country - clientFeedback - clientReviewsCount - clientJobsPosted - clientPastHires - clientPaymentVerificationStatus - searchCategory - searchOccupation - searchKeyword - } - } - } - } - } - `; - - const jobResponses: IEmployeeJobPost[] = []; - - let isContinue: boolean; - let after = ''; - - const filter: EmployeeJobPostFilter = { - isActive: { - is: true, - }, - isArchived: { - is: false, - }, - ...(filters && filters.isApplied - ? { - isApplied: { - is: JSON.parse(filters.isApplied) - } - } - : {}), - ...(filters && filters.jobDateCreated - ? { - jobDateCreated: filters.jobDateCreated, - } - : {}), - ...(filters && filters.title - ? { - jobPost: { - title: { - iLike: `%${filters.title}%`, - }, - }, - } - : {}), - ...(filters && filters.jobType - ? { - jobType: { - in: filters.jobType, - }, - } - : {}), - ...(filters && filters.jobStatus - ? { - jobStatus: { - in: filters.jobStatus, - }, - } - : {}), - ...(filters && filters.jobSource - ? { - providerCode: { - in: filters.jobSource, - }, - } - : {}), - }; - - if (employeeIdFilter) { - const employeeId = await this.getEmployeeGauzyAIId(employeeIdFilter); - filter.employeeId = { - eq: employeeId, - }; - } - - console.log(`Applying filter: ${JSON.stringify(filter)}`); - - const graphQLPageSize = 50; - - // e.g. if it's page 7 and limit is 10, it mean we need to load first 70 records, i.e. do 2 trips to server because each trip get 50 records - const loadCounts = Math.ceil( - (data.page * data.limit) / graphQLPageSize - ); - - console.log(`Round trips to Gauzy API: ${loadCounts}`); - - let currentCount = 1; - - let totalCount: number; - - do { - const result: ApolloQueryResult = await this._client.query({ - query: employeesQuery, - variables: { - after: after, - first: graphQLPageSize, - sorting: [ - { - field: 'jobDateCreated', - direction: 'DESC', - }, - ], - filter: filter, - }, - }); - - console.log(result.errors); - console.log(result.data); - - const jobsResponse = result.data.employeeJobPosts.edges.map( - (it) => { - const rec = it.node; - - const res: IEmployeeJobPost = { - /** Employee Job Post Matching ID */ - id: rec.id, - - employeeId: rec.employee.externalEmployeeId, - employee: undefined, - jobPostId: rec.jobPost.id, - jobPost: rec.jobPost, - - jobDateCreated: rec.jobDateCreated, - providerCode: rec.providerCode, - providerJobId: rec.providerJobId, - jobStatus: rec.jobStatus - ? JobPostStatusEnum[rec.jobStatus] - : undefined, - jobType: rec.jobType - ? JobPostTypeEnum[rec.jobType] - : undefined, - - isApplied: rec.isApplied, - appliedDate: rec.appliedDate, - isActive: rec.isActive, - isArchived: rec.isArchived, - createdAt: rec.createdAt, - updatedAt: rec.updatedAt, - }; - - return res; - } - ); - - isContinue = result.data.employeeJobPosts.pageInfo.hasNextPage && currentCount < loadCounts; - after = result.data.employeeJobPosts.pageInfo.endCursor; - totalCount = result.data.employeeJobPosts.totalCount; - - jobResponses.push(...jobsResponse); - - console.log( - `Found ${jobsResponse.length} job records. IsContinue: ${isContinue}. After: ${after}` - ); - - currentCount++; - } while (isContinue); - - // Note: possible to do additional client side filtering like below: - // jobResponses = _.filter(jobResponses, (it) => it.isActive === true && it.isArchived === false); - - console.log( - `getEmployeesJobPosts. Total Count: ${totalCount}. Page ${data.page}` - ); - - const response: IPagination = { - items: this.paginate(jobResponses, data.limit, data.page), - total: totalCount, - }; - - return response; - } catch (error) { - console.log('Error while getting employee job posts: %s', error?.message); - // this._logger.error(error); - return null; - } - } - - private paginate(array, page_size, page_number) { - // human-readable page numbers usually start with 1, so we reduce 1 in the first argument - return array.slice( - (page_number - 1) * page_size, - page_number * page_size - ); - } - - private async getEmployeeJobPostId( - employeeId: string, - jobPostId: string - ): Promise { - const employeeJobPostsQuery = gql` - query employeeJobPostsByEmployeeIdJobPostId( - $employeeIdFilter: String! - $jobPostIdFilter: String! - ) { - employeeJobPosts( - filter: { - employeeId: { eq: $employeeIdFilter } - jobPostId: { eq: $jobPostIdFilter } - } - ) { - edges { - node { - id - isActive - isArchived - } - } - } - } - `; - - const employeeJobPostsQueryResult = await this._client.query({ - query: employeeJobPostsQuery, - variables: { - employeeIdFilter: employeeId, - jobPostIdFilter: jobPostId, - }, - }); - - const employeeJobPostsResponse = - employeeJobPostsQueryResult.data.employeeJobPosts.edges; - - if (employeeJobPostsResponse && employeeJobPostsResponse.length > 0) { - return employeeJobPostsResponse[0].node.id; - } - - return null; - } - - private async getJobPostId( - providerCode: string, - providerJobId: string - ): Promise { - const jobPostsQuery = gql` - query jobPosts( - $providerCodeFilter: String! - $providerJobIdFilter: String! - ) { - jobPosts( - filter: { - providerCode: { eq: $providerCodeFilter } - providerJobId: { eq: $providerJobIdFilter } - } - ) { - edges { - node { - id - isActive - isArchived - } - } - } - } - `; - - const jobPostsQueryResult = await this._client.query({ - query: jobPostsQuery, - variables: { - providerCodeFilter: providerCode, - providerJobIdFilter: providerJobId, - }, - }); - - const jobPostsResponse = jobPostsQueryResult.data.jobPosts.edges; - - if (jobPostsResponse && jobPostsResponse.length > 0) { - return jobPostsResponse[0].node.id; - } - - return null; - } - - private async getEmployeeGauzyAIId( - externalEmployeeId: string - ): Promise { - const employeesQuery: DocumentNode = gql` - query employeeByExternalEmployeeId( - $externalEmployeeIdFilter: String! - ) { - employees( - filter: { - externalEmployeeId: { eq: $externalEmployeeIdFilter } - } - ) { - edges { - node { - id - externalEmployeeId - } - } - totalCount - } - } - `; - - const employeesQueryResult: ApolloQueryResult = - await this._client.query({ - query: employeesQuery, - variables: { - externalEmployeeIdFilter: externalEmployeeId, - }, - }); - - const employeesResponse = employeesQueryResult.data.employees.edges; - - if (employeesResponse.length > 0) { - return employeesResponse[0].node.id; - } - - return null; - } - - private initClient() { - // Create a custom ApolloLink to modify headers - const authLink = new ApolloLink((operation, forward) => { - const { apiKey, apiSecret, openAiSecretKey, openAiOrganizationId, bearerTokenApi, tenantIdApi } = this._requestConfigProvider.getConfig(); - - // Add your custom headers here - const customHeaders = { - 'Content-Type': 'application/json', - // Set your initial headers here - 'X-APP-ID': this._configService.get('guazyAI.gauzyAiApiKey'), - 'X-API-KEY': this._configService.get('guazyAI.gauzyAiApiSecret'), - - ...(apiKey ? { 'X-APP-ID': apiKey } : {}), - ...(apiSecret ? { 'X-API-KEY': apiSecret } : {}), - ...(openAiSecretKey ? { 'X-OPENAI-SECRET-KEY': openAiSecretKey } : {}), - ...(openAiOrganizationId ? { 'X-OPENAI-ORGANIZATION-ID': openAiOrganizationId } : {}), - - ...(bearerTokenApi ? { 'Authorization': bearerTokenApi } : {}), - ...(tenantIdApi ? { 'Tenant-Id': tenantIdApi } : {}), - }; - - if (this.logging) { - console.log(this._requestConfigProvider.getConfig(), 'Runtime Gauzy AI Integration Config'); - console.log('Custom Run Time Headers: %s', customHeaders); - } - - // Modify the operation context to include the headers - operation.setContext(({ headers }) => ({ - headers: { - ...headers, - ...customHeaders, - }, - })); - // Call the next link in the chain - return forward(operation); - }); - - /** */ - const httpLink = createHttpLink({ - uri: this.gauzyAIGraphQLEndpoint, - fetch - }); - - this._client = new ApolloClient({ - typeDefs: EmployeeJobPostsDocument, - link: authLink.concat(httpLink), - cache: new InMemoryCache(), - defaultOptions: this.defaultOptions, - }); - } - - private init() { - try { - const gauzyAIRESTEndpoint = this._configService.get('guazyAI.gauzyAIRESTEndpoint'); - - console.log(chalk.magenta(`GauzyAI REST Endpoint: ${gauzyAIRESTEndpoint}`)); - - this.gauzyAIGraphQLEndpoint = this._configService.get('guazyAI.gauzyAIGraphQLEndpoint'); - - console.log(chalk.magenta(`GauzyAI GraphQL Endpoint: ${this.gauzyAIGraphQLEndpoint}`)); - - if (this.gauzyAIGraphQLEndpoint && gauzyAIRESTEndpoint) { - this._logger.log('Gauzy AI Endpoints (GraphQL & REST) are configured in the environment'); - - this.initClient(); - - // const testConnectionQuery = async () => { - // try { - // const employeesQuery: DocumentNode = gql` - // query employee { - // employees { - // edges { - // node { - // id - // } - // } - // totalCount - // } - // } - // `; - - // const employeesQueryResult: ApolloQueryResult = - // await this._client.query({ - // query: employeesQuery, - // }); - - // if ( - // employeesQueryResult.networkStatus === - // NetworkStatus.error - // ) { - // this._client = null; - // } - // } catch (err) { - // this._logger.error(err); - // this._client = null; - // } - // }; - - // testConnectionQuery(); - } else { - this._logger.warn( - 'Gauzy AI Endpoints are not configured in the environment' - ); - this._client = null; - } - } catch (err) { - this._logger.warn( - 'Gauzy AI Endpoints are not configured in the environment' - ); - this._logger.error(err); - this._client = null; - } - } - - /** Sync Employee between Gauzy and Gauzy AI - * Creates new Employee in Gauzy AI if it's not yet exists there yet (it try to find by externalEmployeeId field value or by name) - * Update existed Gauzy AI Employee record with new data from Gauzy DB - */ - private async syncEmployee(employee: Employee): Promise { - console.log('-------------------------- Sync Employee --------------------------', employee); - try { - // First, let's search by employee.externalEmployeeId (which is Gauzy employeeId) - let employeesQuery: DocumentNode = gql` - query employeeByExternalEmployeeId( - $externalEmployeeIdFilter: String! - ) { - employees( - filter: { - externalEmployeeId: { eq: $externalEmployeeIdFilter } - } - ) { - edges { - node { - id - externalEmployeeId - } - } - totalCount - } - } - `; - - let employeesQueryResult: ApolloQueryResult = await this._client.query({ - query: employeesQuery, - variables: { - externalEmployeeIdFilter: employee.externalEmployeeId, - }, - }); - - let employeesResponse = employeesQueryResult.data.employees.edges; - - let isAlreadyCreated = employeesResponse.length > 0; - - console.log( - `Is Employee ${employee.externalEmployeeId} already exists in Gauzy AI: ${isAlreadyCreated} by externalEmployeeId field` - ); - - if (!isAlreadyCreated) { - // OK, so we can't find by employee.externalEmployeeId value, let's try to search by name - - employeesQuery = gql` - query employeeByName( - $firstNameFilter: String! - $lastNameFilter: String! - ) { - employees( - filter: { - firstName: { eq: $firstNameFilter } - lastName: { eq: $lastNameFilter } - } - ) { - edges { - node { - id - firstName - lastName - externalEmployeeId - } - } - totalCount - } - } - `; - - employeesQueryResult = await this._client.query({ - query: employeesQuery, - variables: { - firstNameFilter: employee.firstName, - lastNameFilter: employee.lastName, - }, - }); - - employeesResponse = employeesQueryResult.data.employees.edges; - - isAlreadyCreated = employeesResponse.length > 0; - - console.log( - `Is Employee ${employee.externalEmployeeId} already exists in Gauzy AI: ${isAlreadyCreated} by name fields` - ); - - if (!isAlreadyCreated) { - const createEmployeeMutation: DocumentNode = gql` - mutation createOneEmployee( - $input: CreateOneEmployeeInput! - ) { - createOneEmployee(input: $input) { - id - externalEmployeeId - externalTenantId - externalOrgId - upworkOrganizationId - upworkOrganizationName - upworkId - linkedInId - firstName - lastName, - userId - } - } - `; - try { - const newEmployee = await this._client.mutate({ - mutation: createEmployeeMutation, - variables: { - input: { - employee - }, - }, - }); - return newEmployee.data.createOneEmployee; - } catch (error) { - console.log('Error while creating employee: %s', error?.message); - } - } - } - - // update record of employee - const id = employeesResponse[0].node.id; - - const updateEmployeeMutation: DocumentNode = gql` - mutation updateOneEmployee($input: UpdateOneEmployeeInput!) { - updateOneEmployee(input: $input) { - externalEmployeeId - externalTenantId - externalOrgId - upworkOrganizationId - upworkOrganizationName - upworkId - linkedInId - isActive - isArchived - firstName - lastName - userId - } - } - `; - - await this._client.mutate({ - mutation: updateEmployeeMutation, - variables: { - input: { - id: id, - update: employee, - }, - }, - }); - - return employeesResponse[0].node; - } catch (error) { - console.log('Error while synced employee / user: %s', error?.message); - throw new BadRequestException(error?.message); - } - } - - /** - * Sync User between Gauzy and Gauzy AI - * Creates new User in Gauzy AI if it's not yet exists there yet (it try to find by externalUserId field value or by email) - * Update existed Gauzy AI User record with new data from Gauzy DB - */ - private async syncUser(user: User) { - console.log('-------------------------- Sync User --------------------------', user); - // First, let's search by user.externalUserId & user.externalTenantId (which is Gauzy userId) - let userFilterByExternalFieldsQuery: DocumentNode = gql` - query userFilterByExternalFieldsQuery( - $externalUserIdFilter: String! - $externalTenantIdFilter: String! - ) { - users( - filter: { - externalUserId: { eq: $externalUserIdFilter } - externalTenantId: { eq: $externalTenantIdFilter } - } - ) { - edges { - node { - id, - email - username - externalUserId - externalTenantId - } - } - totalCount - } - } - `; - - let usersQueryResult: ApolloQueryResult = await this._client.query({ - query: userFilterByExternalFieldsQuery, - variables: { - externalUserIdFilter: user.externalUserId, - externalTenantIdFilter: user.externalTenantId, - }, - }); - - try { - // Check if there are any GraphQL errors - if (usersQueryResult.errors && usersQueryResult.errors.length > 0) { - // Handle GraphQL errors - const [error] = usersQueryResult.errors; - // You can also access error.extensions for additional error details - - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - // Process the query result if successful - // You can access the data via usersQueryResult.data - let usersResponse = usersQueryResult.data.users.edges; - let isAlreadyCreated = usersQueryResult.data.users.totalCount > 0; - - console.log(`Is User already exists in Gauzy AI: ${isAlreadyCreated} by externalUserId: %s and externalTenantId: %s fields`, user.externalUserId, user.externalTenantId); - - if (!isAlreadyCreated) { - /** Create record of user */ - try { - const createOneUserMutation: DocumentNode = gql` - mutation createOneUser( - $input: CreateOneUserInput! - ) { - createOneUser(input: $input) { - id - firstName - lastName - email - username - hash - externalTenantId - externalUserId - isActive - isArchived - } - } - `; - const newUser = await this._client.mutate({ - mutation: createOneUserMutation, - variables: { - input: { - user - }, - }, - }); - return newUser.data.createOneUser; - } catch (error) { - console.error('Error while creating user: %s', error?.message); - } - } - - console.log(usersResponse[0].node); - /** Update record of user */ - try { - const id = usersResponse[0].node.id; - const updateUserMutation: DocumentNode = gql` - mutation updateOneUser($input: UpdateOneUserInput!) { - updateOneUser(input: $input) { - id - firstName - lastName - email - username - hash - externalTenantId - externalUserId - isActive - isArchived - } - } - `; - const updateUserResponse = await this._client.mutate({ - mutation: updateUserMutation, - variables: { - input: { - id: id, - update: user, - }, - }, - }); - console.log(updateUserResponse.data); - return updateUserResponse.data.updateOneUser; - } catch (error) { - console.error('Error while updating user: %s', error?.message); - this._logger.error(`Error while updating user: ${error?.message}`); - } - } catch (error) { - // Handle other types of errors (e.g., network errors) - console.error('Non-Apollo Client Error while while synced user: %s', error?.message); - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - } - - /** - * Updates the API key of a tenant in the Gauzy AI service. - * - * @param input - The updated API key data. - * @returns The updated tenant API key information. - */ - public async updateOneTenantApiKey(input: UpdateTenantApiKey) { - // Search for the tenant API key by its external API key - let tenantApiKeyFilterByExternalFieldsQuery: DocumentNode = gql` - query tenantKeyFilterByExternalFieldsQuery( - $externalApiKeyFilter: String! - ) { - tenantApiKeys( - filter: { - apiKey: { eq: $externalApiKeyFilter } - } - ) { - edges { - node { - id, - isActive - isArchived - } - } - } - } - `; - - let tenantApiKeysQueryResult: ApolloQueryResult = await this._client.query({ - query: tenantApiKeyFilterByExternalFieldsQuery, - variables: { - externalApiKeyFilter: input.apiKey, - }, - }); - - try { - // Check if there are any GraphQL errors - if (tenantApiKeysQueryResult.errors && tenantApiKeysQueryResult.errors.length > 0) { - // Handle GraphQL errors - const [error] = tenantApiKeysQueryResult.errors; - // You can also access error.extensions for additional error details - - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - - // Process the query result if successful - // You can access the data via tenantApiKeysQueryResult.data - let tenantApiKeysResponse = tenantApiKeysQueryResult.data.tenantApiKeys.edges; - - try { - // Process the query result - const id = tenantApiKeysResponse[0].node.id; - - // Update the tenant API key using a GraphQL mutation - const updateOneTenantApiKeyMutation: DocumentNode = gql` - mutation updateOneTenantApiKey($input: UpdateOneTenantApiKeyInput!) { - updateOneTenantApiKey(input: $input) { - openAiSecretKey - openAiOrganizationId - } - } - `; - - const updateOneTenantApiKeyResponse = await this._client.mutate({ - mutation: updateOneTenantApiKeyMutation, - variables: { - input: { - id: id, - update: { - openAiSecretKey: input.openAiSecretKey, - openAiOrganizationId: input.openAiOrganizationId - }, - }, - }, - }); - - // Return the updated tenant API key information - return updateOneTenantApiKeyResponse.data.updateOneTenantApiKey; - } catch (error) { - console.error('Error while updating Tenant Api Key: %s', error?.message); - this._logger.error(`Error while updating Tenant Api Key: ${error?.message}`); - } - } catch (error) { - // Handle other types of errors (e.g., network errors) - console.error('Non-Apollo Client Error while while synced Tenant Api Key: %s', error?.message); - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - } - - /** - * Retrieves a Tenant based on its external ID. - * - * @param externalTenantId - The external ID of the tenant. - * @returns A Promise resolving to the Tenant instance or null if not found. - */ - private async getTenantByExternalTenantId(externalTenantId: string): Promise { - // Validate externalTenantId - if (!externalTenantId) { - throw new HttpException('External Tenant ID is required', HttpStatus.BAD_REQUEST); - } - - // Define the GraphQL query outside the function - const tenantByExternalTenantIdQuery: DocumentNode = gql` - query tenantByExternalTenantId($externalTenantIdFilter: String!) { - tenants( - filter: { - externalTenantId: { - eq: $externalTenantIdFilter - } - } - ) { - edges { - node { - id - isActive - isArchived - name - externalTenantId - } - } - totalCount - } - } - `; - - try { - // Make the GraphQL query - const tenantsQueryResult: ApolloQueryResult = await this._client.query({ - query: tenantByExternalTenantIdQuery, - variables: { - externalTenantIdFilter: externalTenantId - }, - }); - - // Check if there are any GraphQL errors - if (tenantsQueryResult.errors && tenantsQueryResult.errors.length > 0) { - // Handle GraphQL errors - const [error] = tenantsQueryResult.errors; - // You can also access error.extensions for additional error details - - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - - // Process the query result - const tenantsResponse = tenantsQueryResult.data.tenants; - if (tenantsResponse.totalCount > 0) { - return tenantsResponse.edges[0].node; - } - return null; - } catch (error) { - // Handle other types of errors (e.g., network errors) - console.error('Non-Apollo Client Error while getting tenant: %s', error?.message); - // Use this (using the "options" parameter): - throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); - } - } -} diff --git a/packages/plugins/integration-ai/src/index.ts b/packages/plugins/integration-ai/src/index.ts index 7b0d435a26f..96e10664a9c 100644 --- a/packages/plugins/integration-ai/src/index.ts +++ b/packages/plugins/integration-ai/src/index.ts @@ -1,4 +1,4 @@ -export * from './sdk/gauzy-ai-sdk'; -export * from './gauzy-ai.service'; -export * from './gauzy-ai.module'; -export * from './request-config.provider'; +export * from './lib/sdk/gauzy-ai-sdk'; +export * from './lib/gauzy-ai.service'; +export * from './lib/gauzy-ai.module'; +export * from './lib/request-config.provider'; diff --git a/packages/plugins/integration-ai/src/lib/config/gauzy-ai.ts b/packages/plugins/integration-ai/src/lib/config/gauzy-ai.ts new file mode 100644 index 00000000000..bdb3b242f95 --- /dev/null +++ b/packages/plugins/integration-ai/src/lib/config/gauzy-ai.ts @@ -0,0 +1,12 @@ +import { registerAs } from '@nestjs/config'; + +/** + * Gauzy AI Configuration + */ +export default registerAs('gauzyAI', () => ({ + gauzyAIGraphQLEndpoint: process.env.GAUZY_AI_GRAPHQL_ENDPOINT || null, // GraphQL endpoint for Gauzy AI + gauzyAIRESTEndpoint: process.env.GAUZY_AI_REST_ENDPOINT || null, // REST endpoint for Gauzy AI + gauzyAIRequestTimeout: parseInt(process.env.GAUZY_AI_REQUEST_TIMEOUT) || 60 * 5 * 1000, // Request timeout for Gauzy AI in milliseconds + gauzyAiApiKey: process.env.GAUZY_AI_API_KEY || null, // Gauzy AI API key + gauzyAiApiSecret: process.env.GAUZY_AI_API_SECRET || null // Gauzy AI API secret +})); diff --git a/packages/plugins/integration-ai/src/configuration.interface.ts b/packages/plugins/integration-ai/src/lib/configuration.interface.ts similarity index 60% rename from packages/plugins/integration-ai/src/configuration.interface.ts rename to packages/plugins/integration-ai/src/lib/configuration.interface.ts index 19cc233dc2d..0634827d76f 100644 --- a/packages/plugins/integration-ai/src/configuration.interface.ts +++ b/packages/plugins/integration-ai/src/lib/configuration.interface.ts @@ -1,15 +1,14 @@ - // IApiKeyMethod represents an interface for ApiKey authentication method, with optional ApiKey and ApiSecret properties. export interface IApiKeyMethod { - apiKey?: string; - apiSecret?: string; - openAiSecretKey?: string; - openAiOrganizationId?: string; + apiKey?: string; + apiSecret?: string; + openAiSecretKey?: string; + openAiOrganizationId?: string; } export interface IBearerTokenMethod { - tenantIdApi?: string; - bearerTokenApi?: string; + tenantIdApi?: string; + bearerTokenApi?: string; } export type IConfigurationOptions = IApiKeyMethod & IBearerTokenMethod; @@ -18,18 +17,18 @@ export type IConfigurationOptions = IApiKeyMethod & IBearerTokenMethod; * Represents common HTTP methods as string values. */ export enum HttpMethodEnum { - GET = 'GET', - POST = 'POST', - PUT = 'PUT', - DELETE = 'DELETE', - PATCH = 'PATCH', - HEAD = 'HEAD', - OPTIONS = 'OPTIONS', + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE', + PATCH = 'PATCH', + HEAD = 'HEAD', + OPTIONS = 'OPTIONS' } /** * Represents HTTP request headers as an object where keys are header names and values are header values. */ export interface AxiosRequestHeaders { - [key: string]: string; + [key: string]: string; } diff --git a/packages/plugins/integration-ai/src/constants.ts b/packages/plugins/integration-ai/src/lib/constants.ts similarity index 100% rename from packages/plugins/integration-ai/src/constants.ts rename to packages/plugins/integration-ai/src/lib/constants.ts diff --git a/packages/plugins/integration-ai/src/gauzy-ai.module.ts b/packages/plugins/integration-ai/src/lib/gauzy-ai.module.ts similarity index 51% rename from packages/plugins/integration-ai/src/gauzy-ai.module.ts rename to packages/plugins/integration-ai/src/lib/gauzy-ai.module.ts index 23646b52aa4..486bebffb6b 100644 --- a/packages/plugins/integration-ai/src/gauzy-ai.module.ts +++ b/packages/plugins/integration-ai/src/lib/gauzy-ai.module.ts @@ -12,55 +12,45 @@ import { GAUZY_AI_CONFIG_OPTIONS } from './constants'; HttpModule.registerAsync({ imports: [ConfigModule], useFactory: (config: ConfigService) => ({ - baseURL: config.get('guazyAI.gauzyAIRESTEndpoint'), - timeout: config.get('guazyAI.gauzyAIRequestTimeout'), + baseURL: config.get('gauzyAI.gauzyAIRESTEndpoint'), + timeout: config.get('gauzyAI.gauzyAIRequestTimeout'), maxRedirects: 5, headers: { 'Content-Type': 'application/json', - apiKey: config.get('guazyAI.gauzyAiApiKey'), - apiSecret: config.get('guazyAI.gauzyAiApiSecret'), - }, + apiKey: config.get('gauzyAI.gauzyAiApiKey'), + apiSecret: config.get('gauzyAI.gauzyAiApiSecret') + } }), - inject: [ConfigService], + inject: [ConfigService] }), - ConfigModule.forFeature(gauzyAI), // Make sure to import ConfigModule here + ConfigModule.forFeature(gauzyAI) // Make sure to import ConfigModule here ], controllers: [], - providers: [ - GauzyAIService, - RequestConfigProvider, - ], - exports: [ - GauzyAIService, - RequestConfigProvider, - ], + providers: [GauzyAIService, RequestConfigProvider], + exports: [GauzyAIService, RequestConfigProvider] }) export class GauzyAIModule { /** - * - * @param options - * @returns + * Configure the GauzyAI module for integration with Ever Gauzy Platform. + * @param options Optional configuration options for GauzyAI. + * @returns A dynamic module configuration object. */ static forRoot(options?: IConfigurationOptions): DynamicModule { return { module: GauzyAIModule, - imports: [ - ConfigModule, // Make sure to import ConfigModule here - ], + imports: [ConfigModule], // Make sure to import ConfigModule here providers: [ { provide: GAUZY_AI_CONFIG_OPTIONS, useFactory: (config: ConfigService): IConfigurationOptions => ({ - apiKey: config.get('guazyAI.gauzyAiApiKey'), - apiSecret: config.get('guazyAI.gauzyAiApiSecret'), - ...options, + apiKey: config.get('gauzyAI.gauzyAiApiKey'), + apiSecret: config.get('gauzyAI.gauzyAiApiSecret'), + ...options }), - inject: [ConfigService], - }, - ], - exports: [ - GAUZY_AI_CONFIG_OPTIONS + inject: [ConfigService] + } ], + exports: [GAUZY_AI_CONFIG_OPTIONS] }; } } diff --git a/packages/plugins/integration-ai/src/lib/gauzy-ai.service.ts b/packages/plugins/integration-ai/src/lib/gauzy-ai.service.ts new file mode 100644 index 00000000000..bf94991c40b --- /dev/null +++ b/packages/plugins/integration-ai/src/lib/gauzy-ai.service.ts @@ -0,0 +1,1732 @@ +import { BadRequestException, HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { HttpService } from '@nestjs/axios'; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import qs from 'qs'; +import { Observable, firstValueFrom } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { + CreateEmployeeJobApplication, + User, + Employee, + EmployeeJobPostsDocument, + EmployeeJobPostsQuery, + EmployeeQuery, + UpdateEmployee, + UpdateEmployeeJobPost, + UpworkJobsSearchCriterion, + UserConnection, + Query, + TenantConnection, + UpdateTenantApiKey, + TenantApiKeyConnection, + Tenant, + EmployeeJobPostFilter +} from './sdk/gauzy-ai-sdk'; +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +import fetch from 'cross-fetch'; +import * as chalk from 'chalk'; +import * as FormData from 'form-data'; +import { + ApolloClient, + ApolloQueryResult, + NormalizedCacheObject, + InMemoryCache, + DefaultOptions, + // NetworkStatus, + gql, + createHttpLink, + ApolloLink +} from '@apollo/client/core'; +import { + IEmployeeUpworkJobsSearchCriterion, + IEmployee, + IVisibilityJobPostInput, + IEmployeeJobApplication, + IUpdateEmployeeJobPostAppliedResult, + IEmployeeJobApplicationAppliedResult, + IGetEmployeeJobPostInput, + IPagination, + IEmployeeJobPost, + IJobPost, + IGetEmployeeJobPostFilters, + JobPostStatusEnum, + JobPostTypeEnum, + IEmployeeJobsStatistics +} from '@gauzy/contracts'; +import { RequestConfigProvider } from './request-config.provider'; +import { AxiosRequestHeaders, HttpMethodEnum } from './configuration.interface'; + +export interface ImageAnalysisResult { + success: boolean; + data: { + mimetype: string; + filename: string; + analysis: Array<{ + work: boolean; + description: string; + apps: string[]; + }>; + message?: string; + }; +} + +@Injectable() +export class GauzyAIService { + private readonly _logger = new Logger(GauzyAIService.name); + private _client: ApolloClient; + public logging: boolean = false; + + // For now, we disable Apollo client caching for all GraphQL queries and mutations + private readonly defaultOptions: DefaultOptions = { + watchQuery: { + fetchPolicy: 'no-cache', + errorPolicy: 'ignore' + }, + query: { + fetchPolicy: 'no-cache', + errorPolicy: 'all' + }, + mutate: { + fetchPolicy: 'no-cache', + errorPolicy: 'all' + } + }; + + private gauzyAIGraphQLEndpoint: string; + + constructor( + private readonly _configService: ConfigService, + private readonly _http: HttpService, + private readonly _requestConfigProvider: RequestConfigProvider + ) { + this.init(); + } + + /** + * Send an HTTP request with dynamic configuration. + * + * @param path The URL path for the request. + * @param options Custom Axios request configuration. + * @param method The HTTP method (e.g., GET, POST). + * @returns An Observable that emits the response data or throws an error. + */ + private sendRequest( + path: string, + options: AxiosRequestConfig = {}, + method: string = HttpMethodEnum.GET, + defaultHeaders: AxiosRequestHeaders = { + 'Content-Type': 'application/json' // Define default headers + } + ): Observable> { + /** */ + const { apiKey, apiSecret, openAiSecretKey, openAiOrganizationId, bearerTokenApi, tenantIdApi } = + this._requestConfigProvider.getConfig(); + + // Add your custom headers + const customHeaders = (): AxiosRequestHeaders => ({ + // Define default headers + ...defaultHeaders, + // Add your custom headers here + 'X-APP-ID': this._configService.get('gauzyAI.gauzyAiApiKey'), + 'X-API-KEY': this._configService.get('gauzyAI.gauzyAiApiSecret'), + + /** */ + ...(apiKey ? { 'X-APP-ID': apiKey } : {}), + ...(apiSecret ? { 'X-API-KEY': apiSecret } : {}), + ...(openAiSecretKey ? { 'X-OPENAI-SECRET-KEY': openAiSecretKey } : {}), + ...(openAiOrganizationId ? { 'X-OPENAI-ORGANIZATION-ID': openAiOrganizationId } : {}), + + /** */ + ...(bearerTokenApi ? { Authorization: bearerTokenApi } : {}), + ...(tenantIdApi ? { 'Tenant-Id': tenantIdApi } : {}) + }); + + /** */ + const headers: AxiosRequestHeaders = customHeaders(); + + if (this.logging) { + console.log('Default AxiosRequestConfig Headers: %s', `${JSON.stringify(headers)}`); + } + + // Merge the provided options with the default options + const mergedOptions: AxiosRequestConfig = { + ...options, + // Inside your sendRequest method, use qs.stringify for custom parameter serialization + paramsSerializer: (params) => { + // console.log('Customize the serialization of URL parameters', params); + if (Object.keys(params).length > 0) { + // Customize the serialization of URL parameters as needed + return qs.stringify(params, { arrayFormat: 'repeat' }); + } + } + }; + // console.log('Default AxiosRequestConfig Options: %s', `${JSON.stringify(mergedOptions)}`); + + try { + return this._http.request({ + ...mergedOptions, + url: path, + method, + headers + }); + } catch (error) { + console.log('Error while sending an HTTP request with dynamic configuration.: %s', error); + if (error.response) { + // Handle HTTP error responses + throw new HttpException(error.response.data, error.response.status); + } else { + // Handle other types of errors (e.g., network issues) + throw new HttpException( + 'An error occurred while making the request.', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + } + + /** + * Analyze an image/screenshot using Gauzy AI. + * + * @param files - Array of Buffers representing the uploaded images. + * @returns Promise - The analysis result for the image. + */ + public async analyzeImage(stream: Buffer, file: any): Promise { + // Create FormData and append the image data + const form = new FormData(); + + // Assuming you have an image file or buffer + form.append(`files`, stream, { + filename: file.filename, + contentType: 'application/octet-stream' + }); + + // Set custom headers + const headers = { + ...form.getHeaders(), + 'Content-Length': form.getLengthSync().toString() + // Add any other headers you need + }; + + // Set request options + const options = { + data: form, // Set the request payload + params: {} + }; + + // Call the sendRequest function with the appropriate parameters + return await firstValueFrom( + this.sendRequest('image/process', options, HttpMethodEnum.POST, headers).pipe( + map((resp: AxiosResponse) => resp.data) + ) + ); + } + + /** + * Call pre process method to create new employee job application record. + * + * @param params + * @returns + */ + public async preProcessEmployeeJobApplication(params: IEmployeeJobApplication): Promise { + // First we need to get employee id because we have only externalId + params.employeeId = await this.getEmployeeGauzyAIId(params.employeeId); + + // Call the sendRequest function with the appropriate parameters + return await firstValueFrom( + this.sendRequest( + 'employee/job/application/pre-process', + { + data: params // Set the request payload + }, + HttpMethodEnum.POST + ).pipe( + tap((resp: AxiosResponse) => console.log(resp)), + map((resp: AxiosResponse) => resp.data) + ) + ); + } + + /** + * Generate AI proposal for employee job application + * + * @param employeeJobApplicationId + * @returns + */ + public async generateAIProposalForEmployeeJobApplication(employeeJobApplicationId: string): Promise { + // Call the sendRequest function with the appropriate parameters + return await firstValueFrom( + this.sendRequest( + `employee/job/application/generate-proposal/${employeeJobApplicationId}`, + {}, + HttpMethodEnum.POST + ).pipe( + tap((resp: AxiosResponse) => console.log(resp)), + map((resp: AxiosResponse) => resp.data) + ) + ); + } + + /** + * Get employee job application where proposal generated by AI + * + * @param employeeJobApplicationId + * @returns + */ + public async getEmployeeJobApplication(employeeJobApplicationId: string): Promise { + // Call the sendRequest function with the appropriate parameters + return await firstValueFrom( + this.sendRequest(`employee/job/application/${employeeJobApplicationId}`).pipe( + tap((resp: AxiosResponse) => console.log(resp)), + map((resp: AxiosResponse) => resp.data) + ) + ); + } + + /** + * Get statistic from Gauzy AI about how many jobs are available for given employee + * and to how many of jobs employee already applied and more statistic in the future. + */ + public async getEmployeesStatistics(): Promise { + return []; + } + + /** + * Updates in Gauzy AI if given Employee looking for a jobs or not. + * If not looking, Gauzy AI will NOT return jobs for such employee and will NOT crawl sources for jobs for such employee + * @param employeeId + * @param isJobSearchActive + */ + public async updateEmployeeStatus({ + employeeId, + tenantId, + organizationId, + isJobSearchActive + }: { + employeeId: string; + userId: string; + tenantId: string; + organizationId: string; + isJobSearchActive: boolean; + }): Promise { + if (this._client == null) { + return false; + } + + // First we need to get employee id because we have only externalId + const gauzyAIEmployeeId = await this.getEmployeeGauzyAIId(employeeId); + + console.log( + `updateEmployeeStatus called. EmployeeId: ${employeeId}. Gauzy AI EmployeeId: ${gauzyAIEmployeeId}` + ); + + const update: UpdateEmployee = { + externalEmployeeId: employeeId, + externalTenantId: tenantId, + externalOrgId: organizationId, + isActive: isJobSearchActive, + isArchived: !isJobSearchActive + }; + + const updateEmployeeMutation: DocumentNode = gql` + mutation updateOneEmployee($input: UpdateOneEmployeeInput!) { + updateOneEmployee(input: $input) { + externalEmployeeId + externalTenantId + externalOrgId + isActive + isArchived + } + } + `; + + await this._client.mutate({ + mutation: updateEmployeeMutation, + variables: { + input: { + id: gauzyAIEmployeeId, + update: update + } + } + }); + + return true; + } + + /** + * Apply for a Job + * @param input + * @returns + */ + public async apply(input: IEmployeeJobApplication): Promise { + if (this._client == null) { + return { + ...input, + isRedirectRequired: true + }; + } + + // First we need to get employee id because we have only externalId + const employeeId = await this.getEmployeeGauzyAIId(input.employeeId); + console.log(chalk.green(`Method 'apply' is called. EmployeeId: ${employeeId}`)); + + // Next we need to get a job using providerCode and providerJobId + const jobPostId = await this.getJobPostId(input.providerCode, input.providerJobId); + console.log(chalk.green(`Method 'apply' is called. jobPostId: ${jobPostId}`)); + + // Next, we need to find `public employee job post` table record in Gauzy AI to get id of record. + // We can find by employeeId and jobPostId + + const employeeJobPostId = await this.getEmployeeJobPostId(employeeId, jobPostId); + console.log(chalk.green(`Method 'apply' is called. employeeJobPostId: ${employeeJobPostId}`)); + + if (employeeId && jobPostId && employeeJobPostId) { + const applicationDate = new Date(); + + // ------------------ Create Employee Job Application ------------------ + // This will Apply to the job using Automation system + + const createOneEmployeeJobApplication: CreateEmployeeJobApplication = { + employeeId: employeeId, + jobPostId: jobPostId, + proposal: input.proposal, + rate: input.rate, + // details: input.details, + attachments: input.attachments, + appliedDate: applicationDate, + employeeJobPostId: employeeJobPostId, + isActive: true, + isArchived: false, + providerCode: input.providerCode, + providerJobId: input.providerJobId, + jobType: input.jobType, + jobStatus: input.jobStatus, + terms: input.terms, + qa: input.qa + // Note: isViewedByClient will be updated by our Automation system + // Note: providerJobApplicationId will be set by Automation system when it's applied to the job + }; + + // Call the sendRequest function with the appropriate parameters + const response = await firstValueFrom( + this.sendRequest(`employee/job/application/process`, { + method: HttpMethodEnum.POST, // Set the HTTP method to GET + data: createOneEmployeeJobApplication + }).pipe(map((resp: AxiosResponse) => resp.data)) + ); + + return { + ...response, + isRedirectRequired: false + }; + } else { + return { ...input, isRedirectRequired: true }; + } + } + + /** + * Updates job visibility + * @param hide Should job be hidden or visible. This will set isActive field to false in Gauzy AI + * @param employeeId If employeeId set, job will be set not active only for that specific employee (using EmployeeJobPost record update in Gauzy AI) + * If employeeId is not set, job will be set not active for all employees (using JobPost record update in Gauzy AI) + * @param providerCode e.g. 'upwork' + * @param providerJobId Unique job id in the provider, e.g. in Upwork. If this value is not set, it will update ALL jobs for given provider + */ + public async updateVisibility(input: IVisibilityJobPostInput): Promise { + if (this._client == null) { + return false; + } + + // If it's for specific employee and specific job + if (input.employeeId && input.providerCode && input.providerJobId) { + // First we need to get employee id because we have only externalId + const employeeId = await this.getEmployeeGauzyAIId(input.employeeId); + + console.log(`updateVisibility called. EmployeeId: ${employeeId}`); + + // Next we need to get a job using providerCode and providerJobId + const jobPostId = await this.getJobPostId(input.providerCode, input.providerJobId); + + console.log(`updateVisibility called. jobPostId: ${jobPostId}`); + + // Next, we need to find `public employee job post` table record in Gauzy AI to get id of record. + // We can find by employeeId and jobPostId + + const employeeJobPostId = await this.getEmployeeJobPostId(employeeId, jobPostId); + + console.log(`updateVisibility called. employeeJobPostId: ${employeeJobPostId}`); + + if (employeeId && jobPostId && employeeJobPostId) { + const update: UpdateEmployeeJobPost = { + employeeId: employeeId, + jobPostId: jobPostId, + isActive: !input.hide, + isArchived: input.hide + }; + + const updateEmployeeJobPostMutation: DocumentNode = gql` + mutation updateOneEmployeeJobPost($input: UpdateOneEmployeeJobPostInput!) { + updateOneEmployeeJobPost(input: $input) { + employeeId + jobPostId + isActive + isArchived + isApplied + appliedDate + } + } + `; + + await this._client.mutate({ + mutation: updateEmployeeJobPostMutation, + variables: { + input: { + id: employeeJobPostId, + update: update + } + } + }); + + return true; + } + } else { + // OK, so it's for all jobs for all employees or for all jobs on specific employee + // TODO: implement + } + + return false; + } + + /** + * Create Employee Job Application and updates Employee Job Post record that employee applied for a job + * NOTE: We will not use this method for now. + * + * Inside interface IEmployeeJobApplication we get below fields + * applied: boolean; <- This will set isApplied and appliedDate fields in Gauzy AI + * employeeId: string; <- Employee who applied for a job + * providerCode: string; <- e.g. 'upwork' + * providerJobId: string; <- Unique job id in the provider, e.g. in Upwork + * proposal?: string; <- Proposal text (optional) + * rate?: number; <- Rate (optional, number) + * details?: string; <- Details (optional) + * attachments?: string; <- Attachments (optional, comma separated list of file names) + */ + public async updateApplied(input: IEmployeeJobApplication): Promise { + if (this._client == null) { + return { isRedirectRequired: true }; + } + + // First we need to get employee id because we have only externalId + const employeeId = await this.getEmployeeGauzyAIId(input.employeeId); + console.log(chalk.green(`updateApplied called. EmployeeId: ${employeeId}`)); + + // Next we need to get a job using providerCode and providerJobId + const jobPostId = await this.getJobPostId(input.providerCode, input.providerJobId); + console.log(chalk.green(`updateApplied called. jobPostId: ${jobPostId}`)); + + // Next, we need to find `public employee job post` table record in Gauzy AI to get id of record. + // We can find by employeeId and jobPostId + + const employeeJobPostId = await this.getEmployeeJobPostId(employeeId, jobPostId); + console.log(chalk.green(`updateApplied called. employeeJobPostId: ${employeeJobPostId}`)); + + if (employeeId && jobPostId && employeeJobPostId) { + const applicationDate = new Date(); + + // ------------------ Create Employee Job Application ------------------ + // This will Apply to the job using Automation system + + const createOneEmployeeJobApplication: CreateEmployeeJobApplication = { + employeeId: employeeId, + jobPostId: jobPostId, + proposal: input.proposal, + rate: input.rate, + // details: input.details, + attachments: input.attachments, + appliedDate: applicationDate, + employeeJobPostId: employeeJobPostId, + isActive: true, + isArchived: false, + providerCode: input.providerCode, + providerJobId: input.providerJobId, + jobType: input.jobType, + jobStatus: input.jobStatus, + terms: input.terms, + qa: input.qa + // Note: isViewedByClient will be updated by our Automation system + // Note: providerJobApplicationId will be set by Automation system when it's applied to the job + }; + + const createOneEmployeeJobApplicationMutation: DocumentNode = gql` + mutation createOneEmployeeJobApplication($input: CreateOneEmployeeJobApplicationInput!) { + createOneEmployeeJobApplication(input: $input) { + employeeId + jobPostId + proposal + rate + attachments + appliedDate + employeeJobPostId + isActive + isArchived + providerCode + providerJobId + jobType + jobStatus + terms + qa + } + } + `; + + await this._client.mutate({ + mutation: createOneEmployeeJobApplicationMutation, + variables: { + input: { + employeeJobApplication: createOneEmployeeJobApplication + } + } + }); + + // ------------------ Update Employee Job Post Record ------------------ + // Note: it's just set isApplied and appliedDate fields in Gauzy AI + + const update: UpdateEmployeeJobPost = { + employeeId: employeeId, + jobPostId: jobPostId, + isApplied: input.applied || true, + appliedDate: applicationDate + }; + + const updateEmployeeJobPostMutation: DocumentNode = gql` + mutation updateOneEmployeeJobPost($input: UpdateOneEmployeeJobPostInput!) { + updateOneEmployeeJobPost(input: $input) { + employeeId + jobPostId + isActive + isArchived + isApplied + appliedDate + } + } + `; + + await this._client.mutate({ + mutation: updateEmployeeJobPostMutation, + variables: { + input: { + id: employeeJobPostId, + update: update + } + } + }); + } + + // TODO: here we need to check what returned from Gauzy AI + // Because for some providers (e.g. Upwork), redirect to apply manually required + // But for other providers, apply can work inside Gauzy AI automatically + return { isRedirectRequired: true }; + } + + // We call this on each "Save" operation for matching for employee. + // Both when Preset saved for given employee and when any criteria saved for given employee (new criteria or changes in criteria) + // You should pass `employee` entity for which anything on Matching page was changes + // IMPORTANT: You should ALWAYS pass ALL criteria defined for given employee on Matching page, not only new or changed! + // Best way to call this method, is to reload from Gauzy DB all criteria for given employee before call this method. + // We DO NOT USE DATA YOU PASS FROM UI! + // INSTEAD, We CALL THIS METHOD FROM YOUR CQRS COMMAND HANDLERS when you detect that anything related to matching changes + // But as explained above, we must reload criteria from DB, not use anything you have in the local variables + // (because it might not be full data, but this method requires all data to be synced to Gauzy AI, even if such data was previously already synced) + // How this method will work internally: + // - it will call sync for employee first and if no such employee exists in Gauzy AI, it will create new. If exists, it will update employee properties, e.g. lastName + // - next, it will remove all criteria for employee in Gauzy AI and create new records again for criterions. + // I.e. no update will be done, it will be full replacement + // The reason it's acceptable is because such data changes rarely for given employee, so it's totally fine to recreate it + // NOTE: will need to call this method from multiple different CQRS command handlers! + public async syncGauzyEmployeeJobSearchCriteria( + employee: IEmployee, + criteria: IEmployeeUpworkJobsSearchCriterion[] + ): Promise { + if (this._client == null) { + return false; + } + + console.log( + `syncGauzyEmployeeJobSearchCriteria called. Criteria: ${JSON.stringify( + criteria + )}. Employee: ${JSON.stringify(employee)}` + ); + + try { + const gauzyAIUser: User = await this.syncUser({ + firstName: employee.user.firstName, + lastName: employee.user.lastName, + email: employee.user.email, + username: employee.user.username, + hash: employee.user.hash, + externalTenantId: employee.user.tenantId, + externalUserId: employee.user.id, + isActive: employee.isActive, + isArchived: false + }); + console.log(`Synced User ${JSON.stringify(gauzyAIUser)}`); + /** */ + const gauzyAIEmployee: Employee = await this.syncEmployee({ + externalEmployeeId: employee.id, + externalTenantId: employee.tenantId, + externalOrgId: employee.organizationId, + upworkOrganizationId: employee.organization.upworkOrganizationId, + upworkOrganizationName: employee.organization.upworkOrganizationName, + upworkId: employee.upworkId, + linkedInId: employee.linkedInId, + isActive: employee.isActive, + isArchived: false, + firstName: employee.user.firstName, + lastName: employee.user.lastName, + userId: gauzyAIUser.id + }); + console.log(`Synced Employee ${JSON.stringify(gauzyAIEmployee)}`); + + // let's delete all criteria for Employee + + const deleteAllCriteriaMutation: DocumentNode = gql` + mutation deleteManyUpworkJobsSearchCriteria($input: DeleteManyUpworkJobsSearchCriteriaInput!) { + deleteManyUpworkJobsSearchCriteria(input: $input) { + deletedCount + } + } + `; + + const deleteMutationResult = await this._client.mutate({ + mutation: deleteAllCriteriaMutation, + variables: { + input: { + filter: { + isActive: { + is: true + }, + isArchived: { + is: false + }, + employeeId: { + eq: gauzyAIEmployee.id + } + } + } + } + }); + + console.log( + `Delete Existed Criterions count: ${JSON.stringify( + deleteMutationResult.data.deleteManyUpworkJobsSearchCriteria.deletedCount + )}` + ); + + // now let's create new criteria in Gauzy AI based on Gauzy criterions data + + if (criteria && criteria.length > 0) { + const gauzyAICriteria: UpworkJobsSearchCriterion[] = []; + + criteria.forEach((criterion: IEmployeeUpworkJobsSearchCriterion) => { + gauzyAICriteria.push({ + employeeId: gauzyAIEmployee.id, + isActive: true, + isArchived: false, + jobType: criterion.jobType, + keyword: criterion.keyword, + ...(criterion.category?.name ? { category: criterion.category?.name } : {}), + ...(criterion.categoryId ? { categoryId: criterion.categoryId } : {}), + ...(criterion.occupation?.name ? { occupation: criterion.occupation?.name } : {}), + ...(criterion.occupationId ? { occupationId: criterion.occupationId } : {}) + }); + }); + + const createManyUpworkJobsSearchCriteriaMutation: DocumentNode = gql` + mutation CreateManyUpworkJobsSearchCriteria($input: CreateManyUpworkJobsSearchCriteriaInput!) { + createManyUpworkJobsSearchCriteria(input: $input) { + id + } + } + `; + + const createNewCriteriaResult = await this._client.mutate({ + mutation: createManyUpworkJobsSearchCriteriaMutation, + variables: { + input: { + upworkJobsSearchCriteria: gauzyAICriteria + } + } + }); + + console.log( + `Create New Criteria result: ${JSON.stringify( + createNewCriteriaResult.data.createManyUpworkJobsSearchCriteria + )}` + ); + } + + return true; + } catch (error) { + console.log('Error while synced employee: %s', error?.message); + this._logger.error(error); + return false; + } + } + + /** + * Creates employees in Gauzy AI if not exists yet. If exists, updates fields, including externalEmployeeId + * How it works: + * - search done externalEmployeeId field first in Gauzy AI to be equal to Gauzy employee Id. + * - if no record found in Gauzy AI, it search Gauzy AI employees records by employee name + * - if no record found in Gauzy AI, it creates new employee in Gauzy AI + * + * @param employees + */ + public async syncEmployees(employees: IEmployee[]): Promise { + if (this._client == null) { + return false; + } + try { + await Promise.all( + employees.map(async (employee) => { + try { + try { + /** */ + const gauzyAIUser: User = await this.syncUser({ + firstName: employee.user.firstName, + lastName: employee.user.lastName, + email: employee.user.email, + username: employee.user.username, + hash: employee.user.hash, + externalTenantId: employee.user.tenantId, + externalUserId: employee.user.id, + isActive: employee.isActive, + isArchived: !employee.isActive + }); + console.log(`Synced User ${JSON.stringify(gauzyAIUser)}`); + + try { + /** */ + const gauzyAIEmployee: Employee = await this.syncEmployee({ + externalEmployeeId: employee.id, + externalTenantId: employee.tenantId, + externalOrgId: employee.organizationId, + upworkOrganizationId: employee.organization.upworkOrganizationId, + upworkOrganizationName: employee.organization.upworkOrganizationName, + upworkId: employee.upworkId, + linkedInId: employee.linkedInId, + isActive: employee.isActive, + isArchived: !employee.isActive, + firstName: employee.user.firstName, + lastName: employee.user.lastName, + userId: gauzyAIUser.id + }); + console.log(`Synced Employee ${JSON.stringify(gauzyAIEmployee)}`); + } catch (error) { + console.log('Error while syncing employee: %s', error?.message); + this._logger.error(error); + + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + } catch (error) { + console.log('Error while syncing user: %s', error?.message); + this._logger.error(error); + + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + } catch (error) { + // Handle errors for each employee if necessary + console.error(`Error processing sync employee: ${employee.id}`, error?.message); + this._logger.error(error); + + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + }) + ); + return true; + } catch (error) { + console.log('Error while syncing employees: %s', error?.message); + return false; + } + } + + /** + * Get Jobs available for registered employees + */ + public async getEmployeesJobPosts(data: IGetEmployeeJobPostInput): Promise> { + if (this._client == null) { + return null; + } + + const filters: IGetEmployeeJobPostFilters = data.filters ? data.filters : undefined; + console.log(`getEmployeesJobPosts. Filters ${JSON.stringify(filters)}`); + + const employeeIdFilter = + filters && filters.employeeIds && filters.employeeIds.length > 0 ? filters.employeeIds[0] : undefined; + try { + // TODO: use Query saved in SDK, not hard-code it here. Note: we may add much more fields to that query as we need more info! + const employeesQuery: DocumentNode = gql` + query employeeJobPosts( + $after: ConnectionCursor! + $first: Int! + $filter: EmployeeJobPostFilter! + $sorting: [EmployeeJobPostSort!] + ) { + employeeJobPosts(paging: { after: $after, first: $first }, filter: $filter, sorting: $sorting) { + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + node { + id + isApplied + appliedDate + createdAt + updatedAt + isActive + isArchived + employee { + id + externalEmployeeId + } + providerCode + providerJobId + jobDateCreated + jobStatus + jobType + jobPost { + id + providerCode + providerJobId + title + description + jobDateCreated + jobStatus + jobType + url + budget + duration + workload + skills + category + subcategory + country + clientFeedback + clientReviewsCount + clientJobsPosted + clientPastHires + clientPaymentVerificationStatus + searchCategory + searchOccupation + searchKeyword + } + } + } + } + } + `; + + const jobResponses: IEmployeeJobPost[] = []; + + let isContinue: boolean; + let after = ''; + + const filter: EmployeeJobPostFilter = { + isActive: { + is: true + }, + isArchived: { + is: false + }, + ...(filters && filters.isApplied + ? { + isApplied: { + is: JSON.parse(filters.isApplied) + } + } + : {}), + ...(filters && filters.jobDateCreated + ? { + jobDateCreated: filters.jobDateCreated + } + : {}), + ...(filters && filters.title + ? { + jobPost: { + title: { + iLike: `%${filters.title}%` + } + } + } + : {}), + ...(filters && filters.jobType + ? { + jobType: { + in: filters.jobType + } + } + : {}), + ...(filters && filters.jobStatus + ? { + jobStatus: { + in: filters.jobStatus + } + } + : {}), + ...(filters && filters.jobSource + ? { + providerCode: { + in: filters.jobSource + } + } + : {}) + }; + + if (employeeIdFilter) { + const employeeId = await this.getEmployeeGauzyAIId(employeeIdFilter); + filter.employeeId = { + eq: employeeId + }; + } + + console.log(`Applying filter: ${JSON.stringify(filter)}`); + + const graphQLPageSize = 50; + + // e.g. if it's page 7 and limit is 10, it mean we need to load first 70 records, i.e. do 2 trips to server because each trip get 50 records + const loadCounts = Math.ceil((data.page * data.limit) / graphQLPageSize); + + console.log(`Round trips to Gauzy API: ${loadCounts}`); + + let currentCount = 1; + + let totalCount: number; + + do { + const result: ApolloQueryResult = + await this._client.query({ + query: employeesQuery, + variables: { + after: after, + first: graphQLPageSize, + sorting: [ + { + field: 'jobDateCreated', + direction: 'DESC' + } + ], + filter: filter + } + }); + + console.log(result.errors); + console.log(result.data); + + const jobsResponse = result.data.employeeJobPosts.edges.map((it) => { + const rec = it.node; + + const res: IEmployeeJobPost = { + /** Employee Job Post Matching ID */ + id: rec.id, + + employeeId: rec.employee.externalEmployeeId, + employee: undefined, + jobPostId: rec.jobPost.id, + jobPost: rec.jobPost, + + jobDateCreated: rec.jobDateCreated, + providerCode: rec.providerCode, + providerJobId: rec.providerJobId, + jobStatus: rec.jobStatus ? JobPostStatusEnum[rec.jobStatus] : undefined, + jobType: rec.jobType ? JobPostTypeEnum[rec.jobType] : undefined, + + isApplied: rec.isApplied, + appliedDate: rec.appliedDate, + isActive: rec.isActive, + isArchived: rec.isArchived, + createdAt: rec.createdAt, + updatedAt: rec.updatedAt + }; + + return res; + }); + + isContinue = result.data.employeeJobPosts.pageInfo.hasNextPage && currentCount < loadCounts; + after = result.data.employeeJobPosts.pageInfo.endCursor; + totalCount = result.data.employeeJobPosts.totalCount; + + jobResponses.push(...jobsResponse); + + console.log(`Found ${jobsResponse.length} job records. IsContinue: ${isContinue}. After: ${after}`); + + currentCount++; + } while (isContinue); + + // Note: possible to do additional client side filtering like below: + // jobResponses = _.filter(jobResponses, (it) => it.isActive === true && it.isArchived === false); + + console.log(`getEmployeesJobPosts. Total Count: ${totalCount}. Page ${data.page}`); + + const response: IPagination = { + items: this.paginate(jobResponses, data.limit, data.page), + total: totalCount + }; + + return response; + } catch (error) { + console.log('Error while getting employee job posts: %s', error?.message); + // this._logger.error(error); + return null; + } + } + + private paginate(array, page_size, page_number) { + // human-readable page numbers usually start with 1, so we reduce 1 in the first argument + return array.slice((page_number - 1) * page_size, page_number * page_size); + } + + private async getEmployeeJobPostId(employeeId: string, jobPostId: string): Promise { + const employeeJobPostsQuery = gql` + query employeeJobPostsByEmployeeIdJobPostId($employeeIdFilter: String!, $jobPostIdFilter: String!) { + employeeJobPosts( + filter: { employeeId: { eq: $employeeIdFilter }, jobPostId: { eq: $jobPostIdFilter } } + ) { + edges { + node { + id + isActive + isArchived + } + } + } + } + `; + + const employeeJobPostsQueryResult = await this._client.query({ + query: employeeJobPostsQuery, + variables: { + employeeIdFilter: employeeId, + jobPostIdFilter: jobPostId + } + }); + + const employeeJobPostsResponse = employeeJobPostsQueryResult.data.employeeJobPosts.edges; + + if (employeeJobPostsResponse && employeeJobPostsResponse.length > 0) { + return employeeJobPostsResponse[0].node.id; + } + + return null; + } + + private async getJobPostId(providerCode: string, providerJobId: string): Promise { + const jobPostsQuery = gql` + query jobPosts($providerCodeFilter: String!, $providerJobIdFilter: String!) { + jobPosts( + filter: { providerCode: { eq: $providerCodeFilter }, providerJobId: { eq: $providerJobIdFilter } } + ) { + edges { + node { + id + isActive + isArchived + } + } + } + } + `; + + const jobPostsQueryResult = await this._client.query({ + query: jobPostsQuery, + variables: { + providerCodeFilter: providerCode, + providerJobIdFilter: providerJobId + } + }); + + const jobPostsResponse = jobPostsQueryResult.data.jobPosts.edges; + + if (jobPostsResponse && jobPostsResponse.length > 0) { + return jobPostsResponse[0].node.id; + } + + return null; + } + + private async getEmployeeGauzyAIId(externalEmployeeId: string): Promise { + const employeesQuery: DocumentNode = gql` + query employeeByExternalEmployeeId($externalEmployeeIdFilter: String!) { + employees(filter: { externalEmployeeId: { eq: $externalEmployeeIdFilter } }) { + edges { + node { + id + externalEmployeeId + } + } + totalCount + } + } + `; + + const employeesQueryResult: ApolloQueryResult = await this._client.query({ + query: employeesQuery, + variables: { + externalEmployeeIdFilter: externalEmployeeId + } + }); + + const employeesResponse = employeesQueryResult.data.employees.edges; + + if (employeesResponse.length > 0) { + return employeesResponse[0].node.id; + } + + return null; + } + + private initClient() { + // Create a custom ApolloLink to modify headers + const authLink = new ApolloLink((operation, forward) => { + const { apiKey, apiSecret, openAiSecretKey, openAiOrganizationId, bearerTokenApi, tenantIdApi } = + this._requestConfigProvider.getConfig(); + + // Add your custom headers here + const customHeaders = { + 'Content-Type': 'application/json', + // Set your initial headers here + 'X-APP-ID': this._configService.get('gauzyAI.gauzyAiApiKey'), + 'X-API-KEY': this._configService.get('gauzyAI.gauzyAiApiSecret'), + + ...(apiKey ? { 'X-APP-ID': apiKey } : {}), + ...(apiSecret ? { 'X-API-KEY': apiSecret } : {}), + ...(openAiSecretKey ? { 'X-OPENAI-SECRET-KEY': openAiSecretKey } : {}), + ...(openAiOrganizationId ? { 'X-OPENAI-ORGANIZATION-ID': openAiOrganizationId } : {}), + + ...(bearerTokenApi ? { Authorization: bearerTokenApi } : {}), + ...(tenantIdApi ? { 'Tenant-Id': tenantIdApi } : {}) + }; + + if (this.logging) { + console.log(this._requestConfigProvider.getConfig(), 'Runtime Gauzy AI Integration Config'); + console.log('Custom Run Time Headers: %s', customHeaders); + } + + // Modify the operation context to include the headers + operation.setContext(({ headers }) => ({ + headers: { + ...headers, + ...customHeaders + } + })); + // Call the next link in the chain + return forward(operation); + }); + + /** */ + const httpLink = createHttpLink({ + uri: this.gauzyAIGraphQLEndpoint, + fetch + }); + + this._client = new ApolloClient({ + typeDefs: EmployeeJobPostsDocument, + link: authLink.concat(httpLink), + cache: new InMemoryCache(), + defaultOptions: this.defaultOptions + }); + } + + private init() { + try { + const gauzyAIRESTEndpoint = this._configService.get('gauzyAI.gauzyAIRESTEndpoint'); + + console.log(chalk.magenta(`GauzyAI REST Endpoint: ${gauzyAIRESTEndpoint}`)); + + this.gauzyAIGraphQLEndpoint = this._configService.get('gauzyAI.gauzyAIGraphQLEndpoint'); + + console.log(chalk.magenta(`GauzyAI GraphQL Endpoint: ${this.gauzyAIGraphQLEndpoint}`)); + + if (this.gauzyAIGraphQLEndpoint && gauzyAIRESTEndpoint) { + this._logger.log('Gauzy AI Endpoints (GraphQL & REST) are configured in the environment'); + + this.initClient(); + + // const testConnectionQuery = async () => { + // try { + // const employeesQuery: DocumentNode = gql` + // query employee { + // employees { + // edges { + // node { + // id + // } + // } + // totalCount + // } + // } + // `; + + // const employeesQueryResult: ApolloQueryResult = + // await this._client.query({ + // query: employeesQuery, + // }); + + // if ( + // employeesQueryResult.networkStatus === + // NetworkStatus.error + // ) { + // this._client = null; + // } + // } catch (err) { + // this._logger.error(err); + // this._client = null; + // } + // }; + + // testConnectionQuery(); + } else { + this._logger.warn('Gauzy AI Endpoints are not configured in the environment'); + this._client = null; + } + } catch (err) { + this._logger.warn('Gauzy AI Endpoints are not configured in the environment'); + this._logger.error(err); + this._client = null; + } + } + + /** Sync Employee between Gauzy and Gauzy AI + * Creates new Employee in Gauzy AI if it's not yet exists there yet (it try to find by externalEmployeeId field value or by name) + * Update existed Gauzy AI Employee record with new data from Gauzy DB + */ + private async syncEmployee(employee: Employee): Promise { + console.log('-------------------------- Sync Employee --------------------------', employee); + try { + // First, let's search by employee.externalEmployeeId (which is Gauzy employeeId) + let employeesQuery: DocumentNode = gql` + query employeeByExternalEmployeeId($externalEmployeeIdFilter: String!) { + employees(filter: { externalEmployeeId: { eq: $externalEmployeeIdFilter } }) { + edges { + node { + id + externalEmployeeId + } + } + totalCount + } + } + `; + + let employeesQueryResult: ApolloQueryResult = await this._client.query({ + query: employeesQuery, + variables: { + externalEmployeeIdFilter: employee.externalEmployeeId + } + }); + + let employeesResponse = employeesQueryResult.data.employees.edges; + + let isAlreadyCreated = employeesResponse.length > 0; + + console.log( + `Is Employee ${employee.externalEmployeeId} already exists in Gauzy AI: ${isAlreadyCreated} by externalEmployeeId field` + ); + + if (!isAlreadyCreated) { + // OK, so we can't find by employee.externalEmployeeId value, let's try to search by name + + employeesQuery = gql` + query employeeByName($firstNameFilter: String!, $lastNameFilter: String!) { + employees(filter: { firstName: { eq: $firstNameFilter }, lastName: { eq: $lastNameFilter } }) { + edges { + node { + id + firstName + lastName + externalEmployeeId + } + } + totalCount + } + } + `; + + employeesQueryResult = await this._client.query({ + query: employeesQuery, + variables: { + firstNameFilter: employee.firstName, + lastNameFilter: employee.lastName + } + }); + + employeesResponse = employeesQueryResult.data.employees.edges; + + isAlreadyCreated = employeesResponse.length > 0; + + console.log( + `Is Employee ${employee.externalEmployeeId} already exists in Gauzy AI: ${isAlreadyCreated} by name fields` + ); + + if (!isAlreadyCreated) { + const createEmployeeMutation: DocumentNode = gql` + mutation createOneEmployee($input: CreateOneEmployeeInput!) { + createOneEmployee(input: $input) { + id + externalEmployeeId + externalTenantId + externalOrgId + upworkOrganizationId + upworkOrganizationName + upworkId + linkedInId + firstName + lastName + userId + } + } + `; + try { + const newEmployee = await this._client.mutate({ + mutation: createEmployeeMutation, + variables: { + input: { + employee + } + } + }); + return newEmployee.data.createOneEmployee; + } catch (error) { + console.log('Error while creating employee: %s', error?.message); + } + } + } + + // update record of employee + const id = employeesResponse[0].node.id; + + const updateEmployeeMutation: DocumentNode = gql` + mutation updateOneEmployee($input: UpdateOneEmployeeInput!) { + updateOneEmployee(input: $input) { + externalEmployeeId + externalTenantId + externalOrgId + upworkOrganizationId + upworkOrganizationName + upworkId + linkedInId + isActive + isArchived + firstName + lastName + userId + } + } + `; + + await this._client.mutate({ + mutation: updateEmployeeMutation, + variables: { + input: { + id: id, + update: employee + } + } + }); + + return employeesResponse[0].node; + } catch (error) { + console.log('Error while synced employee / user: %s', error?.message); + throw new BadRequestException(error?.message); + } + } + + /** + * Sync User between Gauzy and Gauzy AI + * Creates new User in Gauzy AI if it's not yet exists there yet (it try to find by externalUserId field value or by email) + * Update existed Gauzy AI User record with new data from Gauzy DB + */ + private async syncUser(user: User) { + console.log('-------------------------- Sync User --------------------------', user); + // First, let's search by user.externalUserId & user.externalTenantId (which is Gauzy userId) + let userFilterByExternalFieldsQuery: DocumentNode = gql` + query userFilterByExternalFieldsQuery($externalUserIdFilter: String!, $externalTenantIdFilter: String!) { + users( + filter: { + externalUserId: { eq: $externalUserIdFilter } + externalTenantId: { eq: $externalTenantIdFilter } + } + ) { + edges { + node { + id + email + username + externalUserId + externalTenantId + } + } + totalCount + } + } + `; + + let usersQueryResult: ApolloQueryResult = await this._client.query({ + query: userFilterByExternalFieldsQuery, + variables: { + externalUserIdFilter: user.externalUserId, + externalTenantIdFilter: user.externalTenantId + } + }); + + try { + // Check if there are any GraphQL errors + if (usersQueryResult.errors && usersQueryResult.errors.length > 0) { + // Handle GraphQL errors + const [error] = usersQueryResult.errors; + // You can also access error.extensions for additional error details + + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + // Process the query result if successful + // You can access the data via usersQueryResult.data + let usersResponse = usersQueryResult.data.users.edges; + let isAlreadyCreated = usersQueryResult.data.users.totalCount > 0; + + console.log( + `Is User already exists in Gauzy AI: ${isAlreadyCreated} by externalUserId: %s and externalTenantId: %s fields`, + user.externalUserId, + user.externalTenantId + ); + + if (!isAlreadyCreated) { + /** Create record of user */ + try { + const createOneUserMutation: DocumentNode = gql` + mutation createOneUser($input: CreateOneUserInput!) { + createOneUser(input: $input) { + id + firstName + lastName + email + username + hash + externalTenantId + externalUserId + isActive + isArchived + } + } + `; + const newUser = await this._client.mutate({ + mutation: createOneUserMutation, + variables: { + input: { + user + } + } + }); + return newUser.data.createOneUser; + } catch (error) { + console.error('Error while creating user: %s', error?.message); + } + } + + console.log(usersResponse[0].node); + /** Update record of user */ + try { + const id = usersResponse[0].node.id; + const updateUserMutation: DocumentNode = gql` + mutation updateOneUser($input: UpdateOneUserInput!) { + updateOneUser(input: $input) { + id + firstName + lastName + email + username + hash + externalTenantId + externalUserId + isActive + isArchived + } + } + `; + const updateUserResponse = await this._client.mutate({ + mutation: updateUserMutation, + variables: { + input: { + id: id, + update: user + } + } + }); + console.log(updateUserResponse.data); + return updateUserResponse.data.updateOneUser; + } catch (error) { + console.error('Error while updating user: %s', error?.message); + this._logger.error(`Error while updating user: ${error?.message}`); + } + } catch (error) { + // Handle other types of errors (e.g., network errors) + console.error('Non-Apollo Client Error while while synced user: %s', error?.message); + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + } + + /** + * Updates the API key of a tenant in the Gauzy AI service. + * + * @param input - The updated API key data. + * @returns The updated tenant API key information. + */ + public async updateOneTenantApiKey(input: UpdateTenantApiKey) { + // Search for the tenant API key by its external API key + let tenantApiKeyFilterByExternalFieldsQuery: DocumentNode = gql` + query tenantKeyFilterByExternalFieldsQuery($externalApiKeyFilter: String!) { + tenantApiKeys(filter: { apiKey: { eq: $externalApiKeyFilter } }) { + edges { + node { + id + isActive + isArchived + } + } + } + } + `; + + let tenantApiKeysQueryResult: ApolloQueryResult = await this._client.query({ + query: tenantApiKeyFilterByExternalFieldsQuery, + variables: { + externalApiKeyFilter: input.apiKey + } + }); + + try { + // Check if there are any GraphQL errors + if (tenantApiKeysQueryResult.errors && tenantApiKeysQueryResult.errors.length > 0) { + // Handle GraphQL errors + const [error] = tenantApiKeysQueryResult.errors; + // You can also access error.extensions for additional error details + + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + + // Process the query result if successful + // You can access the data via tenantApiKeysQueryResult.data + let tenantApiKeysResponse = tenantApiKeysQueryResult.data.tenantApiKeys.edges; + + try { + // Process the query result + const id = tenantApiKeysResponse[0].node.id; + + // Update the tenant API key using a GraphQL mutation + const updateOneTenantApiKeyMutation: DocumentNode = gql` + mutation updateOneTenantApiKey($input: UpdateOneTenantApiKeyInput!) { + updateOneTenantApiKey(input: $input) { + openAiSecretKey + openAiOrganizationId + } + } + `; + + const updateOneTenantApiKeyResponse = await this._client.mutate({ + mutation: updateOneTenantApiKeyMutation, + variables: { + input: { + id: id, + update: { + openAiSecretKey: input.openAiSecretKey, + openAiOrganizationId: input.openAiOrganizationId + } + } + } + }); + + // Return the updated tenant API key information + return updateOneTenantApiKeyResponse.data.updateOneTenantApiKey; + } catch (error) { + console.error('Error while updating Tenant Api Key: %s', error?.message); + this._logger.error(`Error while updating Tenant Api Key: ${error?.message}`); + } + } catch (error) { + // Handle other types of errors (e.g., network errors) + console.error('Non-Apollo Client Error while while synced Tenant Api Key: %s', error?.message); + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + } + + /** + * Retrieves a Tenant based on its external ID. + * + * @param externalTenantId - The external ID of the tenant. + * @returns A Promise resolving to the Tenant instance or null if not found. + */ + private async getTenantByExternalTenantId(externalTenantId: string): Promise { + // Validate externalTenantId + if (!externalTenantId) { + throw new HttpException('External Tenant ID is required', HttpStatus.BAD_REQUEST); + } + + // Define the GraphQL query outside the function + const tenantByExternalTenantIdQuery: DocumentNode = gql` + query tenantByExternalTenantId($externalTenantIdFilter: String!) { + tenants(filter: { externalTenantId: { eq: $externalTenantIdFilter } }) { + edges { + node { + id + isActive + isArchived + name + externalTenantId + } + } + totalCount + } + } + `; + + try { + // Make the GraphQL query + const tenantsQueryResult: ApolloQueryResult = await this._client.query({ + query: tenantByExternalTenantIdQuery, + variables: { + externalTenantIdFilter: externalTenantId + } + }); + + // Check if there are any GraphQL errors + if (tenantsQueryResult.errors && tenantsQueryResult.errors.length > 0) { + // Handle GraphQL errors + const [error] = tenantsQueryResult.errors; + // You can also access error.extensions for additional error details + + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + + // Process the query result + const tenantsResponse = tenantsQueryResult.data.tenants; + if (tenantsResponse.totalCount > 0) { + return tenantsResponse.edges[0].node; + } + return null; + } catch (error) { + // Handle other types of errors (e.g., network errors) + console.error('Non-Apollo Client Error while getting tenant: %s', error?.message); + // Use this (using the "options" parameter): + throw new HttpException(error?.message, HttpStatus.BAD_REQUEST); + } + } +} diff --git a/packages/plugins/integration-ai/src/lib/request-config.provider.ts b/packages/plugins/integration-ai/src/lib/request-config.provider.ts new file mode 100644 index 00000000000..f696666652c --- /dev/null +++ b/packages/plugins/integration-ai/src/lib/request-config.provider.ts @@ -0,0 +1,48 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IConfigurationOptions } from './configuration.interface'; +import { GAUZY_AI_CONFIG_OPTIONS } from './constants'; + +@Injectable() +export class RequestConfigProvider { + private defaultConfig: IConfigurationOptions = new Object(); + private config: IConfigurationOptions = new Object(); + + constructor( + @Inject(GAUZY_AI_CONFIG_OPTIONS) + protected readonly options: IConfigurationOptions + ) { + this.setDefaultConfig(options); + this.resetConfig(); + } + + /** + * Set the default configuration options. + * @param defaultConfig - The default configuration options to set. + */ + setDefaultConfig(defaultConfig: IConfigurationOptions) { + this.defaultConfig = defaultConfig; + } + + /** + * Reset the configuration options to the default values. + */ + resetConfig() { + this.config = { ...this.defaultConfig }; + } + + /** + * Set the configuration options. + * @param config - The configuration options to set. + */ + setConfig(config: IConfigurationOptions) { + this.config = { ...this.defaultConfig, ...config }; + } + + /** + * Get the current configuration options. + * @returns The current configuration options. + */ + getConfig(): IConfigurationOptions { + return this.config; + } +} diff --git a/packages/plugins/integration-ai/src/sdk/gauzy-ai-sdk.ts b/packages/plugins/integration-ai/src/lib/sdk/gauzy-ai-sdk.ts similarity index 100% rename from packages/plugins/integration-ai/src/sdk/gauzy-ai-sdk.ts rename to packages/plugins/integration-ai/src/lib/sdk/gauzy-ai-sdk.ts diff --git a/packages/plugins/integration-ai/src/sdk/queries/query.graphql b/packages/plugins/integration-ai/src/lib/sdk/queries/query.graphql similarity index 100% rename from packages/plugins/integration-ai/src/sdk/queries/query.graphql rename to packages/plugins/integration-ai/src/lib/sdk/queries/query.graphql diff --git a/packages/plugins/integration-ai/src/sdk/schema.graphql b/packages/plugins/integration-ai/src/lib/sdk/schema.graphql similarity index 100% rename from packages/plugins/integration-ai/src/sdk/schema.graphql rename to packages/plugins/integration-ai/src/lib/sdk/schema.graphql diff --git a/packages/plugins/integration-ai/src/request-config.provider.ts b/packages/plugins/integration-ai/src/request-config.provider.ts deleted file mode 100644 index c4fc1a735d1..00000000000 --- a/packages/plugins/integration-ai/src/request-config.provider.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { IConfigurationOptions } from './configuration.interface'; -import { GAUZY_AI_CONFIG_OPTIONS } from './constants'; - -@Injectable() -export class RequestConfigProvider { - - private defaultConfig: IConfigurationOptions = new Object(); - private config: IConfigurationOptions = new Object(); - - constructor( - @Inject(GAUZY_AI_CONFIG_OPTIONS) - protected readonly options: IConfigurationOptions - ) { - this.setDefaultConfig(options); - this.resetConfig(); - } - - /** - * Set the default configuration options. - * @param defaultConfig - The default configuration options to set. - */ - setDefaultConfig(defaultConfig: IConfigurationOptions) { - this.defaultConfig = defaultConfig; - } - - /** - * Reset the configuration options to the default values. - */ - resetConfig() { - this.config = { ...this.defaultConfig }; - } - - /** - * Set the configuration options. - * @param config - The configuration options to set. - */ - setConfig(config: IConfigurationOptions) { - this.config = { ...this.defaultConfig, ...config }; - } - - /** - * Get the current configuration options. - * @returns The current configuration options. - */ - getConfig(): IConfigurationOptions { - return this.config; - } -} diff --git a/packages/plugins/integration-ai/tsconfig.json b/packages/plugins/integration-ai/tsconfig.json index 999d1cb59b2..3f5e1ef6acf 100644 --- a/packages/plugins/integration-ai/tsconfig.json +++ b/packages/plugins/integration-ai/tsconfig.json @@ -1,13 +1,16 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { - "declaration": true, - "sourceMap": true, - "baseUrl": "./src", - "rootDir": "./src", - "outDir": "./dist", - "types": ["node", "jest"] + "module": "commonjs" }, - "include": ["./src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/packages/plugins/integration-ai/tsconfig.lib.json b/packages/plugins/integration-ai/tsconfig.lib.json index ea6be8e9a50..9436f1db06d 100644 --- a/packages/plugins/integration-ai/tsconfig.lib.json +++ b/packages/plugins/integration-ai/tsconfig.lib.json @@ -1,3 +1,14 @@ { - "extends": "./tsconfig.json" + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "sourceMap": true, + "baseUrl": "./src", + "rootDir": "./src", + "outDir": "./dist", + "types": ["node", "jest"], + "target": "es6" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/plugins/integration-ai/tsconfig.lib.prod.json b/packages/plugins/integration-ai/tsconfig.lib.prod.json new file mode 100644 index 00000000000..1696bce1ffd --- /dev/null +++ b/packages/plugins/integration-ai/tsconfig.lib.prod.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declaration": true, + "sourceMap": false, + "removeComments": true, + "noEmitHelpers": true, + "importHelpers": true, + "target": "es6", + "outDir": "./dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/plugins/integration-ai/tsconfig.spec.json b/packages/plugins/integration-ai/tsconfig.spec.json index 9f405553401..64e5ea1a7d4 100644 --- a/packages/plugins/integration-ai/tsconfig.spec.json +++ b/packages/plugins/integration-ai/tsconfig.spec.json @@ -1,15 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", + "outDir": "./dist/spec", "module": "commonjs", "types": ["jest", "node"] }, - "include": [ - "**/*.spec.ts", - "**/*.spec.tsx", - "**/*.spec.js", - "**/*.spec.jsx", - "**/*.d.ts" - ] + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] } diff --git a/packages/plugins/integration-github/.eslintrc.json b/packages/plugins/integration-github/.eslintrc.json new file mode 100644 index 00000000000..79fd7c1d982 --- /dev/null +++ b/packages/plugins/integration-github/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/plugins/integration-github/.gitignore b/packages/plugins/integration-github/.gitignore new file mode 100644 index 00000000000..fe30e94bebf --- /dev/null +++ b/packages/plugins/integration-github/.gitignore @@ -0,0 +1,6 @@ +# dependencies +node_modules/ + +# misc +npm-debug.log +dist diff --git a/packages/plugins/integration-github/.npmignore b/packages/plugins/integration-github/.npmignore new file mode 100644 index 00000000000..1eb4beb9572 --- /dev/null +++ b/packages/plugins/integration-github/.npmignore @@ -0,0 +1,4 @@ +# .npmignore + +src/ +node_modules/ diff --git a/packages/plugins/integration-github/README.md b/packages/plugins/integration-github/README.md index 5988c8f5a56..936a0373df9 100644 --- a/packages/plugins/integration-github/README.md +++ b/packages/plugins/integration-github/README.md @@ -2,10 +2,10 @@ This library was generated with [Nx](https://nx.dev). -## Running unit tests +## Building -Run `ng test integration-github` to execute the unit tests via [Jest](https://jestjs.io). +Run `yarn run build` to build the library. -## Credits +## Running unit tests -In this plugin, we used code from https://github.com/yieldbits/nestjs (MIT license, https://github.com/yieldbits/nestjs/blob/main/LICENSE). +Run `yarn run test:e2e` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/plugins/integration-github/jest.config.js b/packages/plugins/integration-github/jest.config.js deleted file mode 100644 index 21d336427a1..00000000000 --- a/packages/plugins/integration-github/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - name: 'integration-github', - preset: '../../jest.config.js', - transform: { - '^.+\\.[tj]sx?$': 'ts-jest' - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], - coverageDirectory: '../../coverage/plugins/integration-github' -}; diff --git a/packages/plugins/integration-github/jest.config.ts b/packages/plugins/integration-github/jest.config.ts new file mode 100644 index 00000000000..e296b3235b8 --- /dev/null +++ b/packages/plugins/integration-github/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'integration-github', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }] + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/packages/plugins/integration-github' +}; diff --git a/packages/plugins/integration-github/package.json b/packages/plugins/integration-github/package.json index 99e9bc3c688..462bcb28b0a 100644 --- a/packages/plugins/integration-github/package.json +++ b/packages/plugins/integration-github/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/integration-github", + "name": "@gauzy/plugin-integration-github", "version": "0.1.0", "description": "Ever Gauzy Platform plugin for integration with Github APIs", "author": { @@ -8,6 +8,15 @@ "url": "https://ever.co" }, "license": "AGPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/ever-co/ever-gauzy" + }, + "bugs": { + "url": "https://github.com/ever-co/ever-gauzy/issues" + }, + "homepage": "https://ever.co", + "keywords": [], "private": true, "main": "dist/index.js", "types": "dist/index.d.ts", @@ -22,26 +31,52 @@ "access": "restricted" }, "scripts": { - "test:e2e": "jest --config ./jest.config.js", - "build": "rimraf dist && yarn run compile", - "compile": "tsc -p tsconfig.lib.json" + "test:e2e": "jest --config ./jest.config.ts", + "build": "rimraf dist && tsc -p tsconfig.lib.json", + "build:prod": "rimraf dist && tsc -p tsconfig.lib.prod.json" + }, + "peerDependencies": { + "tslib": "^2.6.2" }, - "keywords": [], "dependencies": { + "@gauzy/common": "^0.1.0", + "@gauzy/config": "^0.1.0", + "@gauzy/contracts": "^0.1.0", + "@gauzy/core": "^0.1.0", + "@gauzy/plugin": "^0.1.0", + "@mikro-orm/nestjs": "^5.2.3", + "@nestjs/axios": "^3.0.2", + "@nestjs/cache-manager": "^2.2.1", "@nestjs/common": "^10.3.7", "@nestjs/core": "^10.3.7", + "@nestjs/cqrs": "^10.2.7", + "@nestjs/typeorm": "^10.0.2", "@octokit/rest": "^20.0.2", + "cache-manager": "^5.3.2", + "@nestjs/swagger": "^7.3.0", "chalk": "4.1.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "express": "^4.18.2", + "moment": "^2.30.1", "octokit": "^3.1.2", "pino-std-serializers": "^6.2.2", "probot": "^12.3.3", + "rxjs": "^7.4.0", "smee-client": "^1.2.3", - "underscore": "^1.13.3" + "typeorm": "^0.3.20", + "underscore": "^1.13.3", + "uuid": "^8.3.0" }, "devDependencies": { + "@types/jest": "^29.4.4", "@types/node": "^20.14.9", "rimraf": "^3.0.2", "typescript": "5.1.6" - } + }, + "engines": { + "node": ">=20.11.1", + "yarn": ">=1.22.19" + }, + "sideEffects": false } diff --git a/packages/plugins/integration-github/project.json b/packages/plugins/integration-github/project.json new file mode 100644 index 00000000000..f5a43362db0 --- /dev/null +++ b/packages/plugins/integration-github/project.json @@ -0,0 +1,48 @@ +{ + "name": "plugin-integration-github", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugins/integration-github/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "./packages/plugins/integration-github/dist", + "tsConfig": "packages/plugins/integration-github/tsconfig.lib.json", + "packageJson": "packages/plugins/integration-github/package.json", + "main": "packages/plugins/integration-github/src/index.ts", + "assets": ["packages/plugins/integration-github/*.md"] + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "node tools/scripts/publish.mjs plugins-integration-github {args.ver} {args.tag}" + }, + "dependsOn": ["build"] + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/plugins/integration-github/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/plugins/integration-github/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + }, + "tags": [] +} diff --git a/packages/plugins/integration-github/src/index.ts b/packages/plugins/integration-github/src/index.ts index 65c4483a49e..665079efd83 100644 --- a/packages/plugins/integration-github/src/index.ts +++ b/packages/plugins/integration-github/src/index.ts @@ -1,7 +1 @@ -export * from './probot.types'; -export * from './probot.module'; -export * from './hook.decorator'; -export * from './probot.helpers'; -export * from './probot.discovery'; - -export * from './octokit.service'; +export * from './lib/integration-github.plugin'; diff --git a/packages/plugins/integration-github/src/lib/github/commands/github-installation.delete.command.ts b/packages/plugins/integration-github/src/lib/github/commands/github-installation.delete.command.ts new file mode 100644 index 00000000000..22a88c1c48c --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/commands/github-installation.delete.command.ts @@ -0,0 +1,5 @@ +import { IntegrationTenant } from '@gauzy/core'; + +export class GithubInstallationDeleteCommand { + constructor(public readonly integration: IntegrationTenant) {} +} diff --git a/packages/core/src/integration/github/commands/handlers/github-installation.delete.handler.ts b/packages/plugins/integration-github/src/lib/github/commands/handlers/github-installation.delete.handler.ts similarity index 63% rename from packages/core/src/integration/github/commands/handlers/github-installation.delete.handler.ts rename to packages/plugins/integration-github/src/lib/github/commands/handlers/github-installation.delete.handler.ts index 89046293b41..f1fdb7b3fbd 100644 --- a/packages/core/src/integration/github/commands/handlers/github-installation.delete.handler.ts +++ b/packages/plugins/integration-github/src/lib/github/commands/handlers/github-installation.delete.handler.ts @@ -1,15 +1,12 @@ -import { CommandHandler, ICommandHandler } from "@nestjs/cqrs"; -import { HttpException, HttpStatus } from "@nestjs/common"; -import { OctokitService } from "@gauzy/integration-github"; -import { arrayToObject } from "core/utils"; -import { GithubInstallationDeleteCommand } from "../github-installation.delete.command"; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { arrayToObject } from '@gauzy/core'; +import { OctokitService } from '../../../probot/octokit.service'; +import { GithubInstallationDeleteCommand } from '../github-installation.delete.command'; @CommandHandler(GithubInstallationDeleteCommand) export class GithubInstallationDeleteCommandHandler implements ICommandHandler { - - constructor( - private readonly _octokitService: OctokitService - ) { } + constructor(private readonly _octokitService: OctokitService) {} /** * Execute the GitHub installation deletion command. @@ -23,7 +20,10 @@ export class GithubInstallationDeleteCommandHandler implements ICommandHandler { - - constructor( - private readonly _githubRepositoryService: GithubRepositoryService - ) { } +export class IntegrationSyncGithubRepositoryCommandHandler + implements ICommandHandler +{ + constructor(private readonly _githubRepositoryService: GithubRepositoryService) {} /** * Execute a synchronization of a GitHub repository for an integration. @@ -25,7 +24,7 @@ export class IntegrationSyncGithubRepositoryCommandHandler implements ICommandHa // Destructure the repository object for better readability const { id: repositoryId, full_name, name, owner, open_issues_count } = repository; - const status: GithubRepositoryStatusEnum = repository.status || GithubRepositoryStatusEnum.SYNCING + const status: GithubRepositoryStatusEnum = repository.status || GithubRepositoryStatusEnum.SYNCING; try { /** diff --git a/packages/core/src/integration/github/commands/handlers/task.update-or-create.handler.ts b/packages/plugins/integration-github/src/lib/github/commands/handlers/task.update-or-create.handler.ts similarity index 79% rename from packages/core/src/integration/github/commands/handlers/task.update-or-create.handler.ts rename to packages/plugins/integration-github/src/lib/github/commands/handlers/task.update-or-create.handler.ts index 26a50d2bff5..54fe67a4656 100644 --- a/packages/core/src/integration/github/commands/handlers/task.update-or-create.handler.ts +++ b/packages/plugins/integration-github/src/lib/github/commands/handlers/task.update-or-create.handler.ts @@ -9,27 +9,28 @@ import { IOrganizationProject, IIntegrationTenant } from '@gauzy/contracts'; -import { RequestContext } from 'core/context'; -import { arrayToObject } from 'core/utils'; -import { OrganizationProjectService } from 'organization-project/organization-project.service'; -import { IntegrationTenantGetCommand } from 'integration-tenant/commands'; -import { IntegrationSyncGithubRepositoryIssueCommand } from 'integration/github/repository/issue/commands'; -import { IntegrationMapSyncEntityCommand } from 'integration-map/commands'; -import { IntegrationMapService } from 'integration-map/integration-map.service'; +import { + IntegrationMapService, + IntegrationMapSyncEntityCommand, + IntegrationTenantGetCommand, + OrganizationProjectService, + RequestContext, + arrayToObject +} from '@gauzy/core'; import { GithubRepositoryIssueService } from './../../repository/issue/github-repository-issue.service'; +import { IntegrationSyncGithubRepositoryIssueCommand } from '../../repository/issue/commands'; import { GithubSyncService } from '../../github-sync.service'; import { GithubTaskUpdateOrCreateCommand } from '../task.update-or-create.command'; @CommandHandler(GithubTaskUpdateOrCreateCommand) export class GithubTaskUpdateOrCreateCommandHandler implements ICommandHandler { - constructor( private readonly _commandBus: CommandBus, private readonly _githubSyncService: GithubSyncService, private readonly _organizationProjectService: OrganizationProjectService, private readonly _integrationMapService: IntegrationMapService, private readonly _githubRepositoryIssueService: GithubRepositoryIssueService - ) { } + ) {} /** * Command handler for the `GithubTaskUpdateOrCreateCommand`, responsible for processing actions when a task is opened in Gauzy. @@ -54,12 +55,12 @@ export class GithubTaskUpdateOrCreateCommandHandler implements ICommandHandler String }) + @IsNotEmpty() + @IsString() + readonly code: string; +} + +/** + * + */ +export class GithubAppInstallDTO implements IGithubAppInstallInput { + @ApiPropertyOptional({ type: () => String }) + @IsNotEmpty() + @IsString() + readonly installation_id: string; + + @ApiPropertyOptional({ type: () => String }) + @IsNotEmpty() + @IsEnum(GithubSetupActionEnum) + readonly setup_action: GithubSetupActionEnum; +} diff --git a/packages/plugins/integration-github/src/lib/github/dto/github-issues-query.dto.ts b/packages/plugins/integration-github/src/lib/github/dto/github-issues-query.dto.ts new file mode 100644 index 00000000000..f1535e5a4b5 --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/dto/github-issues-query.dto.ts @@ -0,0 +1,26 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, Max, Min } from 'class-validator'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { IGithubIssueFindInput } from '@gauzy/contracts'; +import { TenantOrganizationBaseDTO } from '@gauzy/core'; + +export class GithubIssuesQueryDTO extends TenantOrganizationBaseDTO implements IGithubIssueFindInput { + /** + * Limit (paginated) - max number of entities should be taken. + */ + @ApiPropertyOptional({ type: () => 'number', minimum: 0, maximum: 100 }) + @IsOptional() + @Min(0) + @Max(100) + @Transform((params: TransformFnParams) => parseInt(params.value, 10)) + readonly per_page: number; + + /** + * Offset (paginated) where from entities should be taken. + */ + @ApiPropertyOptional({ type: () => 'number', minimum: 0 }) + @IsOptional() + @Min(0) + @Transform((params: TransformFnParams) => parseInt(params.value, 10)) + readonly page: number; +} diff --git a/packages/core/src/integration/github/dto/index.ts b/packages/plugins/integration-github/src/lib/github/dto/index.ts similarity index 100% rename from packages/core/src/integration/github/dto/index.ts rename to packages/plugins/integration-github/src/lib/github/dto/index.ts diff --git a/packages/plugins/integration-github/src/lib/github/dto/process-github-issue-sync.dto.ts b/packages/plugins/integration-github/src/lib/github/dto/process-github-issue-sync.dto.ts new file mode 100644 index 00000000000..b016b89945a --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/dto/process-github-issue-sync.dto.ts @@ -0,0 +1,28 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsObject, IsOptional, IsUUID } from 'class-validator'; +import { ID, IGithubIssue, IGithubSyncIssuePayload, IOrganizationGithubRepository } from '@gauzy/contracts'; +import { TenantOrganizationBaseDTO } from '@gauzy/core'; + +/** + * Data Transfer Object for processing GitHub issue synchronization. + * + * This DTO provides optional properties to handle GitHub issues and repositories during synchronization. + */ +export class ProcessGithubIssueSyncDTO extends TenantOrganizationBaseDTO implements IGithubSyncIssuePayload { + /** Optional array of GitHub issues to synchronize. */ + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsArray() + readonly issues: IGithubIssue[]; + + /** Optional GitHub repository for synchronization. */ + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsObject() + readonly repository: IOrganizationGithubRepository; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() + readonly projectId: ID; +} diff --git a/packages/plugins/integration-github/src/lib/github/github-authorization.controller.ts b/packages/plugins/integration-github/src/lib/github/github-authorization.controller.ts new file mode 100644 index 00000000000..436c0224913 --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/github-authorization.controller.ts @@ -0,0 +1,47 @@ +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 { IGithubIntegrationConfig, Public } from '@gauzy/common'; + +@Controller('/integration/github') +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 github callback query data', HttpStatus.BAD_REQUEST); + } + + /** Github Config Options */ + const { postInstallUrl } = this._config.get('github') as IGithubIntegrationConfig; + + /** 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); + + /** Redirect to the URL */ + if (query.state.startsWith('http')) { + return response.redirect(`${query.state}?${urlParams.toString()}`); + } else { + return response.redirect(`${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/plugins/integration-github/src/lib/github/github-entity-settings.ts b/packages/plugins/integration-github/src/lib/github/github-entity-settings.ts new file mode 100644 index 00000000000..bad86206cbc --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/github-entity-settings.ts @@ -0,0 +1,25 @@ +import { IntegrationEntity } from '@gauzy/contracts'; + +/** + * Default settings for entities that will be synchronized. + * This constant defines the default configuration for entities that are integrated with external systems. + * By default, issues are set to be synchronized. + */ +export const DEFAULT_ENTITY_SETTINGS = [ + { + entity: IntegrationEntity.ISSUE, // Represents an issue entity that is integrated with an external system + sync: true // Indicates that this entity should be synchronized + } +]; + +/** + * Entities that are tied to issues and should be synchronized together. + * This constant defines additional entities that are associated with issues and should be synchronized + * when issues are synchronized. Labels are set to be synchronized by default. + */ +export const ISSUE_TIED_ENTITIES = [ + { + entity: IntegrationEntity.LABEL, // Represents a label entity that is tied to an issue + sync: true // Indicates that this entity should be synchronized + } +]; diff --git a/packages/plugins/integration-github/src/lib/github/github-event.subscriber.ts b/packages/plugins/integration-github/src/lib/github/github-event.subscriber.ts new file mode 100644 index 00000000000..ab18660adc0 --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/github-event.subscriber.ts @@ -0,0 +1,122 @@ +import { Injectable, OnModuleInit, OnModuleDestroy, Logger, HttpStatus, HttpException } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { Subject } from 'rxjs'; +import { catchError, filter, takeUntil, tap } from 'rxjs/operators'; +import { IntegrationEnum } from '@gauzy/contracts'; +import { BaseEntityEventTypeEnum, EventBus, IntegrationEvent, RequestContext, TaskEvent } from '@gauzy/core'; +import { GithubInstallationDeleteCommand, GithubTaskUpdateOrCreateCommand } from './commands'; + +@Injectable() +export class GithubEventSubscriber implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger('GithubEventSubscriber'); + private readonly onDestroy$: Subject = new Subject(); + + constructor(private readonly _eventBus: EventBus, private readonly _commandBus: CommandBus) {} + + /** + * Initializes the module and sets up a subscription to listen for IntegrationEvent events. + * The subscription filters the events to only process those related to GitHub integrations. + * When an event is received, a GithubInstallationDeleteCommand is executed. + */ + async onModuleInit() { + this.setupIntegrationEvent(); + this.setupTaskEvent(); + } + + /** + * Sets up a subscription to listen for IntegrationEvent events. + * Depending on the event type, it will execute the appropriate command. + */ + private setupIntegrationEvent() { + this._eventBus + .ofType(IntegrationEvent) + .pipe( + filter((event: IntegrationEvent) => !!event.entity), + filter((event: IntegrationEvent) => event.entity.integration.provider === IntegrationEnum.GITHUB), + tap(async (event: IntegrationEvent) => { + switch (event.type) { + case BaseEntityEventTypeEnum.DELETED: + const command = new GithubInstallationDeleteCommand(event.entity); + await this._commandBus.execute(command); + break; + default: + this.logger.warn(`Unhandled event type: ${event.type}`); + break; + } + }), + catchError((error) => { + // Handle errors and return an appropriate error response + this.logger.error(`Error processing IntegrationEvent: ${error.message}`, error.message); + + // Throw an HttpException to propagate the error + throw new HttpException( + `Error processing IntegrationEvent: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + }), + takeUntil(this.onDestroy$) + ) + .subscribe(); + } + + /** + * Sets up a subscription to listen for TaskEvent events. + * Depending on the event type, it will execute the appropriate command. + */ + private setupTaskEvent() { + this._eventBus + .ofType(TaskEvent) + .pipe( + tap(async (event: TaskEvent) => { + try { + const { organizationId, projectId } = event.input; + const tenantId = RequestContext.currentTenantId() || event.input.tenantId; + + switch (event.type) { + case BaseEntityEventTypeEnum.CREATED: + case BaseEntityEventTypeEnum.UPDATED: + // Only execute command if projectId exists + if (projectId) { + const command = new GithubTaskUpdateOrCreateCommand(event.entity, { + tenantId, + organizationId, + projectId + }); + await this._commandBus.execute(command); + } + break; + default: + this.logger.warn(`Unhandled event type: ${event.type}`); + break; + } + } catch (error) { + this.logger.error('Error while processing task event', error.message); + throw new HttpException( + `Error while processing task event: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + }), + catchError((error) => { + this.logger.error('Error in event subscription', error.message); + + throw new HttpException( + `Error in event subscription: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + }), + takeUntil(this.onDestroy$) + ) + .subscribe(); + } + + /** + * This method is called when the module is destroyed. + * It emits a value and completes the onDestroy$ subject to ensure + * all subscriptions are properly unsubscribed, preventing memory leaks. + */ + async onModuleDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } +} diff --git a/packages/core/src/integration/github/github-integration.controller.ts b/packages/plugins/integration-github/src/lib/github/github-integration.controller.ts similarity index 93% rename from packages/core/src/integration/github/github-integration.controller.ts rename to packages/plugins/integration-github/src/lib/github/github-integration.controller.ts index 5f54bee6f22..423d6a04de2 100644 --- a/packages/core/src/integration/github/github-integration.controller.ts +++ b/packages/plugins/integration-github/src/lib/github/github-integration.controller.ts @@ -1,30 +1,23 @@ -import { - Controller, - Get, - HttpException, - HttpStatus, - Logger, - Param, - Query, - Req, - UseGuards -} from '@nestjs/common'; +import { Controller, Get, HttpException, HttpStatus, Logger, Param, Query, Req, UseGuards } from '@nestjs/common'; import { Request } from 'express'; -import { OctokitResponse, OctokitService } from '@gauzy/integration-github'; import { IGithubIssue, IGithubRepository, IGithubRepositoryResponse, PermissionsEnum } from '@gauzy/contracts'; -import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; -import { Permissions } from '../../shared/decorators'; -import { UseValidationPipe } from '../../shared/pipes'; -import { TenantOrganizationBaseDTO } from 'core/dto'; +import { + PermissionGuard, + Permissions, + TenantOrganizationBaseDTO, + TenantPermissionGuard, + UseValidationPipe +} from '@gauzy/core'; +import { OctokitResponse, OctokitService } from '../probot/octokit.service'; import { GithubIssuesQueryDTO } from './dto'; @UseGuards(TenantPermissionGuard, PermissionGuard) @Permissions(PermissionsEnum.INTEGRATION_VIEW) -@Controller(':integrationId') +@Controller('/integration/github/:integrationId') export class GitHubIntegrationController { private readonly logger = new Logger('GitHubIntegrationController'); - constructor(private readonly _octokitService: OctokitService) { } + constructor(private readonly _octokitService: OctokitService) {} /** * Get GitHub installation metadata for a specific integration. diff --git a/packages/core/src/integration/github/github-sync.controller.ts b/packages/plugins/integration-github/src/lib/github/github-sync.controller.ts similarity index 92% rename from packages/core/src/integration/github/github-sync.controller.ts rename to packages/plugins/integration-github/src/lib/github/github-sync.controller.ts index e7133e8eec9..85e94644633 100644 --- a/packages/core/src/integration/github/github-sync.controller.ts +++ b/packages/plugins/integration-github/src/lib/github/github-sync.controller.ts @@ -12,19 +12,17 @@ import { } from '@nestjs/common'; import { Request } from 'express'; import { IIntegrationTenant, PermissionsEnum } from '@gauzy/contracts'; -import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; -import { Permissions } from '../../shared/decorators'; -import { UseValidationPipe } from '../../shared/pipes'; +import { PermissionGuard, Permissions, TenantPermissionGuard, UseValidationPipe } from '@gauzy/core'; import { GithubSyncService } from './github-sync.service'; import { ProcessGithubIssueSyncDTO } from './dto'; @UseGuards(TenantPermissionGuard, PermissionGuard) @Permissions(PermissionsEnum.INTEGRATION_VIEW) -@Controller(':integrationId') +@Controller('/integration/github/:integrationId') export class GitHubSyncController { private readonly logger = new Logger('GitHubSyncController'); - constructor(private readonly _githubSyncService: GithubSyncService) { } + constructor(private readonly _githubSyncService: GithubSyncService) {} /** * Handle an HTTP POST request to manually synchronize GitHub issues and labels. diff --git a/packages/plugins/integration-github/src/lib/github/github-sync.service.ts b/packages/plugins/integration-github/src/lib/github/github-sync.service.ts new file mode 100644 index 00000000000..737221efa0c --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/github-sync.service.ts @@ -0,0 +1,742 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import * as moment from 'moment'; +import * as chalk from 'chalk'; +import { Request } from 'express'; +import { + IGithubAutomationIssuePayload, + IGithubIssue, + IGithubIssueLabel, + IGithubSyncIssuePayload, + IGithubInstallationDeletedPayload, + IIntegrationEntitySetting, + IIntegrationEntitySettingTied, + IIntegrationTenant, + IOrganization, + IOrganizationProject, + ITag, + IntegrationEntity, + TaskStatusEnum, + IGithubIssueCreateOrUpdatePayload, + IOrganizationGithubRepository, + IIntegrationMap, + GithubRepositoryStatusEnum, + SYNC_TAG_GAUZY, + SYNC_TAG_GITHUB +} from '@gauzy/contracts'; +import { isNotEmpty, sleep } from '@gauzy/common'; +import { + AutomationLabelSyncCommand, + AutomationTaskSyncCommand, + IntegrationMapSyncIssueCommand, + IntegrationMapSyncLabelCommand, + IntegrationTenantService, + OrganizationProjectService, + RequestContext, + arrayToObject +} from '@gauzy/core'; +import { OctokitService } from '../probot/octokit.service'; +import { GithubRepositoryService } from './repository/github-repository.service'; +import { IntegrationSyncGithubRepositoryIssueCommand } from './repository/issue/commands'; + +@Injectable() +export class GithubSyncService { + private readonly logger = new Logger('GithubSyncService'); + + constructor( + private readonly _commandBus: CommandBus, + private readonly _octokitService: OctokitService, + private readonly _integrationTenantService: IntegrationTenantService, + private readonly _organizationProjectService: OrganizationProjectService, + private readonly _githubRepositoryService: GithubRepositoryService + ) {} + + /** + * Automatically synchronize GitHub issues with a repository. + * + * @param {IIntegrationTenant['id']} integrationId - The ID of the integration tenant. + * @param {IGithubSyncIssuePayload} input - The payload containing GitHub repository details and issues. + * @param {Request} request - The HTTP request object. + * @returns {Promise} A Promise that indicates whether the synchronization was successful. + */ + public async autoSyncGithubIssues( + integrationId: IIntegrationTenant['id'], + input: IGithubSyncIssuePayload, + request: Request + ): Promise { + // 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 + ); + } + + try { + // Extract the 'repository' object from the input payload + const repository: IOrganizationGithubRepository = input.repository; + + try { + // Extract the 'installation_id' from the integration settings + const installation_id = settings['installation_id']; + + // Extract repository details + const { name: repo, owner } = repository; + + // Retrieve GitHub issues for the repository + this.getRepositoryAllIssues(installation_id, owner, repo, (issues: IGithubIssue[]) => { + console.log(chalk.magenta(`Automatically syncing ${issues.length} issues`)); + + // Map the issues to the desired format using '_mapIssuePayload' method + input.issues = this._mapIssuePayload(Array.isArray(issues) ? issues : [issues]); + + // Define a delay of 100 milliseconds + const delay: number = 100; + + // Attempt to synchronize GitHub issues using the 'syncingGithubIssues' method + this.syncingGithubIssues( + integrationId, + input, + delay, + async () => { + // Update the status of the GitHub repository to "Success" (GithubRepositoryStatusEnum.SUCCESSFULLY). + await this._githubRepositoryService.update(repository.id, { + status: GithubRepositoryStatusEnum.SUCCESSFULLY + }); + }, + async () => { + // Handle the error by updating the status of the GitHub repository to "Error" (GithubRepositoryStatusEnum.ERROR). + await this._githubRepositoryService.update(repository.id, { + status: GithubRepositoryStatusEnum.ERROR + }); + } + ); + }); + + return true; // Return true to indicate a successful synchronization. + } catch (error) { + console.log('Error while syncing github issues automatically: %s', error.message); + // Handle the error by updating the status of the GitHub repository to "Error" (GithubRepositoryStatusEnum.ERROR). + await this._githubRepositoryService.update(repository.id, { + status: GithubRepositoryStatusEnum.ERROR + }); + + return false; // Return false to indicate that an error occurred during synchronization. + } + } catch (error) { + // Handle errors gracefully, for example, log them + this.logger.error('Error in sync github issues and labels automatically', error.message); + throw new HttpException( + { message: 'GitHub automatic synchronization failed', error }, + HttpStatus.BAD_REQUEST + ); + } + } + + /** + * Manually synchronize GitHub issues with a repository. + * + * @param {IIntegrationTenant['id']} integrationId - The ID of the integration tenant. + * @param {IGithubSyncIssuePayload} input - The payload containing GitHub repository details and issues. + * @param {Request} request - The HTTP request object. + * @returns {Promise} A Promise indicating whether the synchronization was successful. + */ + public async manualSyncGithubIssues( + integrationId: IIntegrationTenant['id'], + input: IGithubSyncIssuePayload, + request: Request + ): Promise { + try { + // 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 + ); + } + + // Extract the 'repository' object from the input payload + const repository: IOrganizationGithubRepository = input.repository; + + try { + // Set a delay of 0 milliseconds + const delay: number = 0; + + // Attempt to synchronize GitHub issues using the syncGithubIssues method. + await this.syncingGithubIssues(integrationId, input, delay); + + // Update the status of the GitHub repository to "Success" (GithubRepositoryStatusEnum.SUCCESSFULLY). + await this._githubRepositoryService.update(repository.id, { + status: GithubRepositoryStatusEnum.SUCCESSFULLY + }); + + return true; // Return true to indicate a successful synchronization. + } catch (error) { + // Handle the error by updating the status of the GitHub repository to "Error" (GithubRepositoryStatusEnum.ERROR). + await this._githubRepositoryService.update(repository.id, { + status: GithubRepositoryStatusEnum.ERROR + }); + + return false; // Return false to indicate that an error occurred during synchronization. + } + } catch (error) { + // Handle errors gracefully, for example, log them + this.logger.error('Error in sync GitHub issues and labels manually', error.message); + + // Throw an HTTP exception to indicate manual synchronization failure. + throw new HttpException({ message: 'GitHub manual synchronization failed', error }, HttpStatus.BAD_REQUEST); + } + } + + /** + * Synchronize GitHub issues and labels based on entity settings. + * + * @param integrationId - The ID of the integration tenant. + * @param input - The payload containing information required for synchronization. + * @throws {HttpException} Throws an HTTP exception if synchronization fails. + */ + public async syncingGithubIssues( + integrationId: IIntegrationTenant['id'], + input: IGithubSyncIssuePayload, + delay: number = 100, + successCallback?: (success: boolean) => void, + errorCallback?: (error: boolean) => void + ): Promise { + try { + const { organizationId, repository } = input; + + const tenantId = RequestContext.currentTenantId() || input.tenantId; + const issues: IGithubIssue[] = Array.isArray(input.issues) ? input.issues : [input.issues]; + + // Step 1: Retrieve integration settings tied to the specified organization + const { entitySettings } = await this._integrationTenantService.findOneByIdString(integrationId, { + where: { + tenantId, + organizationId, + isActive: true, + isArchived: false + }, + relations: { + entitySettings: { + tiedEntities: true + } + } + }); + + try { + // Step 2: Initialize an array for integration mapping + let integrationMaps: IIntegrationMap[] = []; + + // Step 3: Synchronize data based on entity settings + for await (const entitySetting of entitySettings) { + switch (entitySetting.entity) { + case IntegrationEntity.ISSUE: + // Step 4: Issue synchronization + const issueSetting: IIntegrationEntitySetting = entitySetting; + if (!!issueSetting.sync) { + for await (const issue of issues) { + console.log(chalk.green(`Processing Issue Sync: %s`), issue.id); + const { id, title, state, body } = issue; + + let tags: ITag[] = []; + try { + // Step 5: Label synchronization settings + const labelSetting: IIntegrationEntitySetting = entitySetting.tiedEntities.find( + ({ entity }: IIntegrationEntitySettingTied) => + entity === IntegrationEntity.LABEL + ); + if (!!labelSetting && labelSetting.sync) { + // Step 6: Sync GitHub Issue Labels + tags = await this.syncGithubLabelsByIssueNumber({ + organizationId, + tenantId, + integrationId, + repository, + issue + }); + } + } catch (error) { + console.error( + 'Failed to fetch GitHub labels for the repository issue:', + error.message + ); + } + + // Step 7: Synchronized GitHub Repository Issue. + const { repositoryId } = repository; + await this._commandBus.execute( + new IntegrationSyncGithubRepositoryIssueCommand( + { + tenantId, + organizationId, + integrationId + }, + repositoryId, + issue + ) + ); + + // Step 8: Execute a command to initiate the synchronization process + const triggeredEvent = false; + const integrationMap = await this._commandBus.execute( + new IntegrationMapSyncIssueCommand( + { + entity: { + title, + description: body, + status: state as TaskStatusEnum, + public: repository.private, + projectId: input['projectId'] || null, + tags, + organizationId, + tenantId + }, + sourceId: id.toString(), + integrationId, + organizationId, + tenantId + }, + triggeredEvent + ) + ); + integrationMaps.push(integrationMap); + + /** 100ms Pause or Delay for sync new sync issue */ + await sleep(delay); + } + } + break; + } + } + + // Step 9: Update Integration Last Synced Date + await this._integrationTenantService.update(integrationId, { + lastSyncedAt: moment().toDate() + }); + + // Call the success callback function if provided + if (successCallback) { + successCallback(true); + } + + // Step 10: Return integration mapping + return integrationMaps; + } catch (error) { + console.log('Error while syncing github issues: ', error.message); + // Call the error callback function if provided + if (errorCallback) { + errorCallback(false); + } + + return false; + } + } catch (error) { + // Handle errors gracefully, for example, log them + this.logger.error('Error in sync github issues and labels manual', error.message); + throw new HttpException({ message: 'GitHub manual synchronization failed', error }, HttpStatus.BAD_REQUEST); + } + } + + /** + * Synchronize GitHub labels for a specific repository issue based on integration settings. + * + * @param organizationId - The ID of the organization. + * @param tenantId - The ID of the organization's tenant. + * @param integrationId - The ID of the GitHub integration. + * @param repository - Information about the GitHub repository for which labels are synchronized. + * @param issue - The GitHub issue for which labels are synchronized. + * @returns A promise that resolves to the result of the label synchronization process, which is an array of tags. + */ + private async syncGithubLabelsByIssueNumber({ + organizationId, + tenantId, + integrationId, + repository, + issue + }: { + organizationId: IOrganization['id']; + tenantId: IOrganization['tenantId']; + integrationId: IIntegrationTenant['id']; + repository: IOrganizationGithubRepository; + issue: IGithubIssue; + }): Promise { + try { + // Retrieve integration settings + const integration = await this._integrationTenantService.findOneByIdString(integrationId, { + where: { isActive: true, isArchived: false }, + relations: { settings: true } + }); + + const settings = arrayToObject(integration.settings, 'settingsName', 'settingsValue'); + const { name: repo, owner } = repository; + + // Check for integration settings and installation ID + if (settings && settings.installation_id) { + const installation_id = settings.installation_id; + + // Get the labels associated with the GitHub issue + let labels = issue.labels; + + // List of labels to check and create if missing + const labelsToCheck = [SYNC_TAG_GITHUB, SYNC_TAG_GAUZY]; + const labelsToCreate = labelsToCheck.filter( + (name) => !labels.find((label: IGithubIssueLabel) => label.name === name) + ); + + // Check if specific labels exist on a GitHub issue and create them if missing. + if (isNotEmpty(labelsToCreate)) { + try { + const response = await this._octokitService.addLabelsForIssue(installation_id, { + owner, + repo, + issue_number: issue.number, + labels: labelsToCreate + }); + labels = response.data; + } catch (error) { + console.log(chalk.red(`Error while creating missing labels with payload: %s`), error); + } + } + + // Sync labels and return an array of tags + return await Promise.all( + labels.map(async (label: IGithubIssueLabel) => { + const { id: sourceId, name, color, description } = label; + console.log(chalk.magenta(`Syncing GitHub Automation Issue Label: %s`), label); + + return await this._commandBus.execute( + new IntegrationMapSyncLabelCommand({ + entity: { + name, + color, + description, + isSystem: label.default + }, + sourceId: sourceId.toString(), + integrationId, + organizationId, + tenantId + }) + ); + }) + ); + } + return []; + } catch (error) { + // Handle errors and return an appropriate error response + this.logger.error('Failed to fetch GitHub labels for the repository issue', error.message); + return []; + } + } + + /** + * Syncs automation issues for a GitHub repository. + * + * @param integration - The GitHub integration settings. + * @param input - The payload containing information for the synchronization. + */ + public async syncAutomationIssue(input: IGithubAutomationIssuePayload) { + const { integration, repository, issue } = input; + const { entitySettings } = integration; + + try { + /** Extract necessary data from integration */ + const tenantId = integration['tenantId']; + const organizationId = integration['organizationId']; + const integrationId = integration['id']; + + /** Get a list of projects for the repository */ + const projects = await this._organizationProjectService.getProjectsByGithubRepository(repository.id, { + organizationId, + tenantId, + integrationId + }); + + for await (const project of projects) { + console.log(chalk.magenta(`Syncing GitHub Automation Issues for Project: %s`), project.name); + // Check if the issue should be synchronized for this project + if (!!this.shouldSyncIssue(project, issue)) { + const issues: IGithubIssue[] = this._mapIssuePayload(Array.isArray(issue) ? issue : [issue]); + const projectId = project.id; + + // Synchronize data based on entity settings + for await (const entitySetting of entitySettings) { + switch (entitySetting.entity) { + case IntegrationEntity.ISSUE: + /** Issues Sync */ + const issueSetting: IIntegrationEntitySetting = entitySetting; + if (!!issueSetting.sync) { + for await (const issue of issues) { + const { id, title, state, body, labels = [] } = issue; + + // Initialize an array to store tags + let tags: ITag[] = []; + + // Check for label synchronization settings + try { + const labelSetting = entitySetting.tiedEntities.find( + ({ entity }: IIntegrationEntitySettingTied) => + entity === IntegrationEntity.LABEL + ); + + if (!!labelSetting && labelSetting.sync) { + /** Sync GitHub Issue Labels */ + tags = await Promise.all( + labels.map(async (label: IGithubIssueLabel) => { + const { id: labelId, name, color, description } = label; + /** */ + return await this._commandBus.execute( + new AutomationLabelSyncCommand( + { + entity: { + name, + color, + description, + isSystem: label.default + }, + sourceId: labelId.toString(), + integrationId, + organizationId, + tenantId + }, + IntegrationEntity.LABEL + ) + ); + }) + ); + } + } catch (error) { + console.error( + 'Failed to fetch GitHub labels for the repository issue:', + error.message + ); + } + + // Step 7: Synchronized GitHub repository issue. + const repositoryId = repository.id; + await this._commandBus.execute( + new IntegrationSyncGithubRepositoryIssueCommand( + { + tenantId, + organizationId, + integrationId + }, + repositoryId, + issue + ) + ); + + try { + // Synchronize the issue as a task + return await this._commandBus.execute( + new AutomationTaskSyncCommand( + { + entity: { + title, + description: body, + status: state as TaskStatusEnum, + public: repository.private, + prefix: project.name.substring(0, 3) || null, + projectId, + organizationId, + tenantId, + tags + }, + sourceId: id.toString(), + integrationId, + integration, + organizationId, + tenantId + }, + IntegrationEntity.ISSUE + ) + ); + } catch (error) { + this.logger.error(`Failed to sync automation github task: ${id}`, error); + } + } + } + } + } + } + } + } catch (error) { + this.logger.error( + `Failed to fetch repository: ${repository.id} integration with specific project too sync issue: ${issue.id}`, + error + ); + } + } + + /** + * Determines whether an issue should be synchronized based on project settings. + * + * @param project - The project configuration. + * @param issue - The GitHub issue to be synchronized. + * @returns A boolean indicating whether the issue should be synchronized. + */ + private shouldSyncIssue(project: IOrganizationProject, issue: IGithubIssue): boolean { + if (!project || !project.isTasksAutoSync) { + return false; + } + if (project.isTasksAutoSyncOnLabel) { + return !!issue.labels.find((label) => label.name.trim() === project.syncTag.trim()); + } + return true; + } + + /** + * Deletes a GitHub installation and its associated integration. + * + * @param payload - An object containing the installation and its associated integration. + */ + public async installationDeleted(payload: IGithubInstallationDeletedPayload) { + try { + // Extract the integration ID from the provided integration object + const integrationId = payload.integration.id; + // ToDo delete sync repository with specific project + // const repositories = payload.repositories; + + // Delete the integration associated with the installation + await this._integrationTenantService.delete(integrationId); + } catch (error) { + // Handle errors + this.logger.error( + `Failed to delete GitHub integration for installation: ${payload.installation?.id}`, + error + ); + } + } + + /** + * Map GitHub issue payload data to the required format. + * + * @param issues - An array of GitHub issues. + * @returns An array of mapped issue payload data. + */ + private _mapIssuePayload(issues: IGithubIssue[]): any[] { + return issues.map(({ id, number, title, state, body, labels }) => ({ + id, + number, + title, + state, + body, + labels + })); + } + + /** + * Create or Update a GitHub issue on a repository using the specified installation ID. + * + * @param installationId - The GitHub installation ID. + * @param data - The data for the GitHub issue, including repo, owner, title, body, and labels. + * @returns A promise that resolves to the response from GitHub. + */ + public async createOrUpdateIssue(installationId: number, data: IGithubIssueCreateOrUpdatePayload): Promise { + try { + // Check if a valid installation ID is provided + if (!installationId) { + throw new HttpException('Invalid request parameter', HttpStatus.UNAUTHORIZED); + } + + // Prepare the payload for opening or updating the issue + const payload = { + repo: data.repo, + owner: data.owner, + title: data.title, + body: data.body, + labels: data.labels + }; + + // Create or update the installation issue using the octokit service + if (data.issue_number) { + // Issue number is provided, update the existing issue + const issue_number = data.issue_number; + + try { + // Check if the issue exists + await this._octokitService.getIssueByIssueNumber(installationId, { + repo: payload.repo, + owner: payload.owner, + issue_number: issue_number + }); + + // Issue exists, update it + const issue = await this._octokitService.updateIssue(installationId, issue_number, payload); + return issue.data; + } catch (error) { + // Issue doesn't exist, create a new one + const issue = await this._octokitService.openIssue(installationId, payload); + return issue.data; + } + } else { + // Issue number is not provided, create a new issue + const issue = await this._octokitService.openIssue(installationId, payload); + return issue.data; + } + } catch (error) { + // Handle errors and return an appropriate error response + this.logger.error('Error while creating/updating an issue in GitHub', error.message); + throw new HttpException( + `Error while creating/updating an issue in GitHub: ${error.message}`, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * Retrieves all issues from a GitHub repository using the GitHub API with pagination. + * + * @param installation_id - The installation ID for the GitHub App. + * @param owner - The owner (user or organization) of the GitHub repository. + * @param repo - The name of the GitHub repository. + * @returns A Promise that resolves to an array of GitHub issues. + */ + async getRepositoryAllIssues( + installation_id: number, + owner: string, + repo: string, + callback?: (issues: IGithubIssue[]) => void + ): Promise { + const per_page = 100; // Number of issues per page (GitHub API maximum is 100) + const issues: IGithubIssue[] = []; + let page = 1; + let hasMoreIssues = true; + + // Use a while to simplify pagination + while (hasMoreIssues) { + try { + // Fetch issues for the current page + const response = await this._octokitService.getRepositoryIssues(installation_id, { + owner, + repo, + page, + per_page + }); + if (Array.isArray(response.data) && response.data.length > 0) { + // Append the retrieved issues to the result array + issues.push(...response.data); + // Check if there are more issues on the next page + hasMoreIssues = response.data.length === per_page; + } else { + // No more issues to retrieve + hasMoreIssues = false; + } + // Increment the page number for the next request + page++; + } catch (error) { + console.error('Error fetching issues:', error); + break; // Exit the loop on error + } + } + + // Call the callback function if provided + if (callback) { + callback(issues); + } + + return issues; + } +} diff --git a/packages/core/src/integration/github/github.config.ts b/packages/plugins/integration-github/src/lib/github/github.config.ts similarity index 100% rename from packages/core/src/integration/github/github.config.ts rename to packages/plugins/integration-github/src/lib/github/github.config.ts diff --git a/packages/core/src/integration/github/github.controller.ts b/packages/plugins/integration-github/src/lib/github/github.controller.ts similarity index 87% rename from packages/core/src/integration/github/github.controller.ts rename to packages/plugins/integration-github/src/lib/github/github.controller.ts index 6420fd0667b..199730cde95 100644 --- a/packages/core/src/integration/github/github.controller.ts +++ b/packages/plugins/integration-github/src/lib/github/github.controller.ts @@ -1,16 +1,14 @@ import { Controller, Post, Body, UseGuards, HttpException, HttpStatus, HttpCode } from '@nestjs/common'; import { PermissionsEnum } from '@gauzy/contracts'; -import { PermissionGuard, TenantPermissionGuard } from '../../shared/guards'; -import { Permissions } from '../../shared/decorators'; -import { UseValidationPipe } from '../../shared/pipes'; +import { PermissionGuard, Permissions, TenantPermissionGuard, UseValidationPipe } from '@gauzy/core'; import { GithubService } from './github.service'; import { GithubAppInstallDTO, GithubOAuthDTO } from './dto'; @UseGuards(TenantPermissionGuard, PermissionGuard) @Permissions(PermissionsEnum.INTEGRATION_VIEW) -@Controller() +@Controller('/integration/github') export class GitHubController { - constructor(private readonly _githubService: GithubService) { } + constructor(private readonly _githubService: GithubService) {} /** * diff --git a/packages/plugins/integration-github/src/lib/github/github.hooks.controller.ts b/packages/plugins/integration-github/src/lib/github/github.hooks.controller.ts new file mode 100644 index 00000000000..d8f4b238b8b --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/github.hooks.controller.ts @@ -0,0 +1,71 @@ +import { Controller } from '@nestjs/common'; +import { Context } from 'probot'; +import { Public } from '@gauzy/common'; +import { Hook } from '../probot/hook.decorator'; +import { GithubHooksService } from './github.hooks.service'; + +@Public() +@Controller('/integration/github/webhook') +export class GitHubHooksController { + constructor(private readonly _githubHooksService: GithubHooksService) {} + + /** + * Handles the 'installation.deleted' event. + * + * @param context - The context object containing information about the event. + */ + @Hook(['installation.deleted']) + async installationDeleted(context: Context) { + if (!context.isBot) { + await this._githubHooksService.installationDeleted(context); + } + } + + /** + * Handles the 'issues.opened' event. + * + * @param context - The context object containing information about the event. + */ + @Hook(['issues.opened']) + async issuesOpened(context: Context) { + if (!context.isBot) { + await this._githubHooksService.issuesOpened(context); + } + } + + /** + * Handles the 'issues.edited' event. + * + * @param context - The context object containing information about the event. + */ + @Hook(['issues.edited']) + async issuesEdited(context: Context) { + if (!context.isBot) { + await this._githubHooksService.issuesEdited(context); + } + } + + /** + * Handles the 'issues.labeled' event. + * + * @param context - The context object containing information about the event. + */ + @Hook(['issues.labeled']) + async issuesLabeled(context: Context) { + if (!context.isBot) { + await this._githubHooksService.issuesLabeled(context); + } + } + + /** + * Handles the 'issues.labeled' event. + * + * @param context - The context object containing information about the event. + */ + @Hook(['issues.unlabeled']) + async issuesUnlabeled(context: Context) { + if (!context.isBot) { + await this._githubHooksService.issuesUnlabeled(context); + } + } +} diff --git a/packages/plugins/integration-github/src/lib/github/github.hooks.service.ts b/packages/plugins/integration-github/src/lib/github/github.hooks.service.ts new file mode 100644 index 00000000000..74e0b3aaa05 --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/github.hooks.service.ts @@ -0,0 +1,216 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { Context } from 'probot'; +import * as chalk from 'chalk'; +import { + GithubPropertyMapEnum, + IGithubInstallation, + IGithubIssue, + IGithubRepository, + IIntegrationSetting +} from '@gauzy/contracts'; +import { IntegrationSettingGetCommand, IntegrationSettingGetManyCommand } from '@gauzy/core'; +import { GithubSyncService } from './github-sync.service'; + +@Injectable() +export class GithubHooksService { + private readonly logger = new Logger('GithubHooksService'); + + constructor(private readonly _commandBus: CommandBus, private readonly _githubSyncService: GithubSyncService) {} + + /** + * Handles the 'installation.deleted' event by deleting a GitHub installation, + * its associated repositories, and the integration setting. + * + * @param context - The context object containing event information. + */ + async installationDeleted(context: Context) { + // Extract necessary data from the context + const installation = context.payload['installation'] as IGithubInstallation; + const repositories = context.payload['repositories'] as IGithubRepository[]; + + try { + const installation_id = installation.id; + // Retrieve the integration settings associated with the GitHub installation. + const settings = await this._commandBus.execute( + new IntegrationSettingGetManyCommand({ + where: { + settingsName: GithubPropertyMapEnum.INSTALLATION_ID, + settingsValue: installation_id, + isActive: true, + isArchived: false, + integration: { + isActive: true, + isArchived: false + } + }, + relations: { + integration: { + settings: true, + entitySettings: { + tiedEntities: true + } + } + } + }) + ); + return await Promise.all( + settings.map(async (setting: IIntegrationSetting) => { + if (!setting || !setting.integration) { + // No integration or setting found; no action needed. + return; + } + + const integration = setting.integration; + + // Delete the GitHub integration associated with the installation and its repositories + await this._githubSyncService.installationDeleted({ + installation, + integration, + repositories + }); + }) + ); + } catch (error) { + // Handle errors + this.logger.error(`Failed to delete GitHub integration for installation: ${installation?.id}`, error); + } + } + + /** + * Handles the 'issues.opened' event from GitHub, syncs automation issues and labels. + * + * @param context - The GitHub webhook event context. + */ + async issuesOpened(context: Context) { + try { + // Extract necessary data from the context + const installation = context.payload['installation'] as IGithubInstallation; + const issue = context.payload['issue'] as IGithubIssue; + const repository = context.payload['repository'] as IGithubRepository; + console.log(chalk.magenta(`Syncing GitHub Automation ID: %s`), context.payload['action']); + + /** Synchronizes automation issues for a GitHub installation. */ + await this.syncAutomationIssue({ installation, issue, repository }); + } catch (error) { + this.logger.error('Failed to sync in issues and labels', error.message); + } + } + + /** + * Handles the 'issues.edited' event from GitHub, syncs automation issues and labels. + * + * @param context - The GitHub webhook event context. + */ + async issuesEdited(context: Context) { + try { + // Extract necessary data from the context + const installation = context.payload['installation'] as IGithubInstallation; + const issue = context.payload['issue'] as IGithubIssue; + const repository = context.payload['repository'] as IGithubRepository; + console.log(chalk.magenta(`Syncing GitHub Automation ID: %s`), context.payload['action']); + + /** Synchronizes automation issues for a GitHub installation. */ + await this.syncAutomationIssue({ installation, issue, repository }); + } catch (error) { + this.logger.error('Failed to sync in issues and labels', error.message); + } + } + + /** + * Handles the 'issuesLabeled' event from GitHub. + * + * @param context - The GitHub webhook event context. + */ + async issuesLabeled(context: Context) { + try { + // Extract necessary data from the context + const installation = context.payload['installation'] as IGithubInstallation; + const issue = context.payload['issue'] as IGithubIssue; + const repository = context.payload['repository'] as IGithubRepository; + console.log(chalk.magenta(`Syncing GitHub Automation ID: %s`), context.payload['action']); + + /** Synchronizes automation issues for a GitHub installation. */ + await this.syncAutomationIssue({ installation, issue, repository }); + } catch (error) { + this.logger.error('Failed to sync in issues and labels', error.message); + } + } + + /** + * Handles the 'issuesUnlabeled' event from GitHub. + * + * @param context - The GitHub webhook event context. + */ + async issuesUnlabeled(context: Context) { + try { + // Extract necessary data from the context + const installation = context.payload['installation'] as IGithubInstallation; + const issue = context.payload['issue'] as IGithubIssue; + const repository = context.payload['repository'] as IGithubRepository; + console.log(chalk.magenta(`Syncing GitHub Automation ID: %s`), context.payload['action']); + + /** Synchronizes automation issues for a GitHub installation. */ + await this.syncAutomationIssue({ installation, issue, repository }); + } catch (error) { + this.logger.error('Failed to sync in issues and labels', error.message); + } + } + + /** + * Synchronizes automation issues for a GitHub installation. + * + * @param param0 - An object containing installation, issue, and repository information. + */ + private async syncAutomationIssue({ + installation, + issue, + repository + }: { + installation: IGithubInstallation; + issue: IGithubIssue; + repository: IGithubRepository; + }): Promise { + try { + const setting: IIntegrationSetting = await this.getInstallationSetting(installation); + if (!!setting && !!setting.integration) { + const integration = setting.integration; + await this._githubSyncService.syncAutomationIssue({ integration, issue, repository }); + } + } catch (error) { + this.logger.error(`Failed to sync GitHub automation issue: ${installation?.id}`, error.message); + } + } + + /** + * Retrieves integration settings associated with a specific GitHub installation. + * + * @param installation - The GitHub installation for which to retrieve settings. + * @returns A promise that resolves to the integration setting or rejects with an error. + */ + private async getInstallationSetting(installation: IGithubInstallation): Promise { + try { + const installation_id = installation.id; + // Retrieve the integration setting associated with the GitHub installation. + return await this._commandBus.execute( + new IntegrationSettingGetCommand({ + where: { + settingsName: GithubPropertyMapEnum.INSTALLATION_ID, + settingsValue: installation_id, + isActive: true, + isArchived: false, + integration: { isActive: true, isArchived: false } + }, + relations: { + integration: { + settings: true, + entitySettings: { tiedEntities: true } + } + } + }) + ); + } catch (error) { + this.logger.error(`Failed to fetch GitHub installation setting: ${installation?.id}`, error.message); + } + } +} diff --git a/packages/core/src/integration/github/github.middleware.ts b/packages/plugins/integration-github/src/lib/github/github.middleware.ts similarity index 71% rename from packages/core/src/integration/github/github.middleware.ts rename to packages/plugins/integration-github/src/lib/github/github.middleware.ts index d984918672e..9e1b7aed5b9 100644 --- a/packages/core/src/integration/github/github.middleware.ts +++ b/packages/plugins/integration-github/src/lib/github/github.middleware.ts @@ -4,8 +4,7 @@ import { Cache } from 'cache-manager'; import { Request, Response, NextFunction } from 'express'; import { isNotEmpty } from '@gauzy/common'; import { IIntegrationSetting, IntegrationEnum } from '@gauzy/contracts'; -import { arrayToObject } from '../../core/utils'; -import { IntegrationTenantService } from 'integration-tenant/integration-tenant.service'; +import { IntegrationTenantService, arrayToObject } from '@gauzy/core'; @Injectable() export class GithubMiddleware implements NestMiddleware { @@ -14,7 +13,7 @@ export class GithubMiddleware implements NestMiddleware { constructor( @Inject(CACHE_MANAGER) private cacheManager: Cache, private readonly _integrationTenantService: IntegrationTenantService - ) { } + ) {} /** * @@ -37,7 +36,9 @@ export class GithubMiddleware implements NestMiddleware { try { // Fetch integration settings from the service if (this.logging) { - console.log(`Getting Gauzy integration settings from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}`); + console.log( + `Getting Gauzy integration settings from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}` + ); } const cacheKey = `integrationTenantSettings_${tenantId}_${organizationId}_${integrationId}`; @@ -46,7 +47,9 @@ export class GithubMiddleware implements NestMiddleware { if (!integrationTenantSettings) { if (this.logging) { - console.log(`Gauzy integration settings NOT loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}`); + console.log( + `Gauzy integration settings NOT loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}` + ); } const fromDb = await this._integrationTenantService.findOneByIdString(integrationId, { @@ -68,16 +71,20 @@ export class GithubMiddleware implements NestMiddleware { if (fromDb && fromDb.settings) { integrationTenantSettings = fromDb.settings; - const ttl = 5 * 60 * 1000 // 5 min caching period for GitHub Integration Tenant Settings + const ttl = 5 * 60 * 1000; // 5 min caching period for GitHub Integration Tenant Settings await this.cacheManager.set(cacheKey, integrationTenantSettings, ttl); if (this.logging) { - console.log(`Gauzy integration settings loaded from DB and stored in Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}`); + console.log( + `Gauzy integration settings loaded from DB and stored in Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}` + ); } } } else { if (this.logging) { - console.log(`Gauzy integration settings loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}`); + console.log( + `Gauzy integration settings loaded from Cache for tenantId: ${tenantId}, organizationId: ${organizationId}, integrationId: ${integrationId}` + ); } } @@ -92,13 +99,19 @@ export class GithubMiddleware implements NestMiddleware { }); } } catch (error) { - console.log(`Error while getting integration (${IntegrationEnum.GITHUB}) tenant inside middleware: %s`, error?.message); + console.log( + `Error while getting integration (${IntegrationEnum.GITHUB}) tenant inside middleware: %s`, + error?.message + ); console.log(request.path, request.url); } } } } catch (error) { - console.log(`Error while getting integration (${IntegrationEnum.GITHUB}) tenant inside middleware: %s`, error?.message); + console.log( + `Error while getting integration (${IntegrationEnum.GITHUB}) tenant inside middleware: %s`, + error?.message + ); console.log(request.path, request.url); } diff --git a/packages/core/src/integration/github/github.module.ts b/packages/plugins/integration-github/src/lib/github/github.module.ts similarity index 67% rename from packages/core/src/integration/github/github.module.ts rename to packages/plugins/integration-github/src/lib/github/github.module.ts index 8048a82ed72..83e9f226e26 100644 --- a/packages/core/src/integration/github/github.module.ts +++ b/packages/plugins/integration-github/src/lib/github/github.module.ts @@ -3,13 +3,19 @@ import { HttpModule } from '@nestjs/axios'; import { CqrsModule } from '@nestjs/cqrs'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { RolePermissionModule } from '../../role-permission/role-permission.module'; -import { IntegrationModule } from '../../integration/integration.module'; -import { IntegrationTenantModule } from '../../integration-tenant/integration-tenant.module'; -import { IntegrationSettingModule } from '../../integration-setting/integration-setting.module'; -import { IntegrationMapModule } from '../../integration-map/integration-map.module'; -import { OrganizationProjectModule } from '../../organization-project/organization-project.module'; +import { + IntegrationMapModule, + IntegrationModule, + IntegrationSettingModule, + IntegrationTenantModule, + OrganizationProjectModule, + PluginCommonModule, + RolePermissionModule +} from '@gauzy/core'; +import { environment } from '@gauzy/config'; +import { ProbotModule } from '../probot/probot.module'; import { CommandHandlers } from './commands/handlers'; +import { GithubEventSubscriber } from './github-event.subscriber'; import { GitHubAuthorizationController } from './github-authorization.controller'; import { GitHubIntegrationController } from './github-integration.controller'; import { GitHubController } from './github.controller'; @@ -27,18 +33,36 @@ import { GithubRepositoryIssueService } from './repository/issue/github-reposito import { TypeOrmOrganizationGithubRepositoryRepository } from './repository/repository'; import { TypeOrmOrganizationGithubRepositoryIssueRepository } from './repository/issue/repository'; +// +const { github } = environment; + @Module({ imports: [ + HttpModule, + CqrsModule, TypeOrmModule.forFeature([OrganizationGithubRepository, OrganizationGithubRepositoryIssue]), MikroOrmModule.forFeature([OrganizationGithubRepository, OrganizationGithubRepositoryIssue]), - HttpModule, + PluginCommonModule, RolePermissionModule, forwardRef(() => OrganizationProjectModule), forwardRef(() => IntegrationModule), forwardRef(() => IntegrationTenantModule), forwardRef(() => IntegrationSettingModule), forwardRef(() => IntegrationMapModule), - CqrsModule + // Probot Configuration + ProbotModule.forRoot({ + isGlobal: true, + // Webhook URL in GitHub will be: https://api.gauzy.co/api/integration/github/webhook + path: 'integration/github/webhook', + config: { + /** Client Configuration */ + clientId: github.clientId, + clientSecret: github.clientSecret, + appId: github.appId, + privateKey: github.appPrivateKey, + webhookSecret: github.webhookSecret + } + }) ], controllers: [ GitHubAuthorizationController, @@ -49,52 +73,53 @@ import { TypeOrmOrganizationGithubRepositoryIssueRepository } from './repository GitHubRepositoryController ], providers: [ + // Define middleware heres + GithubMiddleware, + // Define services heres + GithubEventSubscriber, GithubService, GithubSyncService, GithubHooksService, GithubRepositoryService, GithubRepositoryIssueService, - // Define middleware heres - GithubMiddleware, + // Define repositories heres TypeOrmOrganizationGithubRepositoryRepository, TypeOrmOrganizationGithubRepositoryIssueRepository, // Define handlers heres ...CommandHandlers ], - exports: [ - TypeOrmOrganizationGithubRepositoryRepository, - TypeOrmOrganizationGithubRepositoryIssueRepository - ], + exports: [TypeOrmOrganizationGithubRepositoryRepository, TypeOrmOrganizationGithubRepositoryIssueRepository] }) export class GithubModule implements NestModule { /** + * Configures middleware for specific routes. * - * @param consumer + * @param consumer - The middleware consumer to apply the middlewares. */ configure(consumer: MiddlewareConsumer) { - // Apply middlewares to specific controllers + // Apply the GithubMiddleware to specific routes consumer.apply(GithubMiddleware).forRoutes( + // Define routes and HTTP methods for which the middleware should be applied { path: '/integration/github/:integrationId/metadata', - method: RequestMethod.GET, + method: RequestMethod.GET }, { path: '/integration/github/:integrationId/repositories', - method: RequestMethod.GET, + method: RequestMethod.GET }, { path: '/integration/github/:integrationId/:owner/:repo/issues', - method: RequestMethod.GET, + method: RequestMethod.GET }, - /** */ { path: '/integration/github/:integrationId/manual-sync/issues', - method: RequestMethod.POST, + method: RequestMethod.POST }, { path: '/integration/github/:integrationId/auto-sync/issues', - method: RequestMethod.POST, + method: RequestMethod.POST } - ); // Apply to specific routes and methods + ); // Apply the middleware to specific routes and methods } } diff --git a/packages/core/src/integration/github/github.service.ts b/packages/plugins/integration-github/src/lib/github/github.service.ts similarity index 66% rename from packages/core/src/integration/github/github.service.ts rename to packages/plugins/integration-github/src/lib/github/github.service.ts index 4f8ba8da676..4c36f45647d 100644 --- a/packages/core/src/integration/github/github.service.ts +++ b/packages/plugins/integration-github/src/lib/github/github.service.ts @@ -12,11 +12,9 @@ import { IntegrationEnum, SYNC_TAG_GITHUB } from '@gauzy/contracts'; -import { RequestContext } from 'core/context'; -import { IntegrationTenantUpdateOrCreateCommand } from 'integration-tenant/commands'; -import { IntegrationService } from 'integration/integration.service'; +import { IntegrationService, IntegrationTenantUpdateOrCreateCommand, RequestContext } from '@gauzy/core'; +import { DEFAULT_ENTITY_SETTINGS, ISSUE_TIED_ENTITIES } from './github-entity-settings'; import { GITHUB_ACCESS_TOKEN_URL } from './github.config'; -import { DEFAULT_ENTITY_SETTINGS, ISSUE_TIED_ENTITIES, } from './github-entity-settings'; const { github } = environment; @Injectable() @@ -27,7 +25,7 @@ export class GithubService { private readonly _http: HttpService, private readonly _commandBus: CommandBus, private readonly _integrationService: IntegrationService - ) { } + ) {} /** * Adds a GitHub App installation by validating input data, fetching an access token, and creating integration tenant settings. @@ -36,9 +34,7 @@ export class GithubService { * @returns A promise that resolves to the access token data. * @throws Error if any step of the process fails. */ - public async addGithubAppInstallation( - input: IGithubAppInstallInput - ): Promise { + public async addGithubAppInstallation(input: IGithubAppInstallInput): Promise { try { // Validate the input data (You can use class-validator for validation) if (!input || !input.installation_id || !input.setup_action) { @@ -50,9 +46,7 @@ export class GithubService { /** Find the GitHub integration */ const integration = await this._integrationService.findOneByOptions({ - where: { - provider: IntegrationEnum.GITHUB - } + where: { provider: IntegrationEnum.GITHUB } }); const tiedEntities = ISSUE_TIED_ENTITIES.map((entity) => ({ @@ -79,9 +73,7 @@ export class GithubService { new IntegrationTenantUpdateOrCreateCommand( { name: IntegrationEnum.GITHUB, - integration: { - provider: IntegrationEnum.GITHUB, - }, + integration: { provider: IntegrationEnum.GITHUB }, tenantId, organizationId }, @@ -96,21 +88,21 @@ export class GithubService { settings: [ { settingsName: GithubPropertyMapEnum.INSTALLATION_ID, - settingsValue: installation_id, + settingsValue: installation_id }, { settingsName: GithubPropertyMapEnum.SETUP_ACTION, - settingsValue: setup_action, + settingsValue: setup_action }, { settingsName: GithubPropertyMapEnum.SYNC_TAG, - settingsValue: SYNC_TAG_GITHUB, + settingsValue: SYNC_TAG_GITHUB } ].map((setting) => ({ ...setting, tenantId, organizationId - })), + })) } ) ); @@ -149,67 +141,69 @@ export class GithubService { urlParams.append('client_secret', github.clientSecret); urlParams.append('code', code); - const tokens$ = this._http.post(GITHUB_ACCESS_TOKEN_URL, urlParams, { - headers: { - accept: 'application/json', - } - }).pipe( - switchMap(async ({ data }) => { - if (!data.error) { - // Token retrieval was successful, return the token data - return await this._commandBus.execute( - new IntegrationTenantUpdateOrCreateCommand( - { - name: IntegrationEnum.GITHUB, - integration: { - provider: IntegrationEnum.GITHUB, - }, - tenantId, - organizationId - }, - { - name: IntegrationEnum.GITHUB, - integration, - tenantId, - organizationId, - entitySettings: [], - isActive: true, - isArchived: false, - settings: [ - { - settingsName: GithubPropertyMapEnum.ACCESS_TOKEN, - settingsValue: data.access_token - }, - { - settingsName: GithubPropertyMapEnum.EXPIRES_IN, - settingsValue: data.expires_in.toString() - }, - { - settingsName: GithubPropertyMapEnum.REFRESH_TOKEN, - settingsValue: data.refresh_token - }, - { - settingsName: GithubPropertyMapEnum.REFRESH_TOKEN_EXPIRES_IN, - settingsValue: data.refresh_token_expires_in.toString() + const tokens$ = this._http + .post(GITHUB_ACCESS_TOKEN_URL, urlParams, { + headers: { + accept: 'application/json' + } + }) + .pipe( + switchMap(async ({ data }) => { + if (!data.error) { + // Token retrieval was successful, return the token data + return await this._commandBus.execute( + new IntegrationTenantUpdateOrCreateCommand( + { + name: IntegrationEnum.GITHUB, + integration: { + provider: IntegrationEnum.GITHUB }, - { - settingsName: GithubPropertyMapEnum.TOKEN_TYPE, - settingsValue: data.token_type - } - ].map((setting) => ({ - ...setting, tenantId, organizationId - })), - } - ) - ); - } else { - // Token retrieval failed, Throw an error to handle the failure - throw new BadRequestException('Token retrieval failed', data); - } - }) - ); + }, + { + name: IntegrationEnum.GITHUB, + integration, + tenantId, + organizationId, + entitySettings: [], + isActive: true, + isArchived: false, + settings: [ + { + settingsName: GithubPropertyMapEnum.ACCESS_TOKEN, + settingsValue: data.access_token + }, + { + settingsName: GithubPropertyMapEnum.EXPIRES_IN, + settingsValue: data.expires_in.toString() + }, + { + settingsName: GithubPropertyMapEnum.REFRESH_TOKEN, + settingsValue: data.refresh_token + }, + { + settingsName: GithubPropertyMapEnum.REFRESH_TOKEN_EXPIRES_IN, + settingsValue: data.refresh_token_expires_in.toString() + }, + { + settingsName: GithubPropertyMapEnum.TOKEN_TYPE, + settingsValue: data.token_type + } + ].map((setting) => ({ + ...setting, + tenantId, + organizationId + })) + } + ) + ); + } else { + // Token retrieval failed, Throw an error to handle the failure + throw new BadRequestException('Token retrieval failed', data); + } + }) + ); return await firstValueFrom(tokens$); } catch (error) { // Handle errors and return an appropriate error response diff --git a/packages/core/src/integration/github/repository/dto/index.ts b/packages/plugins/integration-github/src/lib/github/repository/dto/index.ts similarity index 100% rename from packages/core/src/integration/github/repository/dto/index.ts rename to packages/plugins/integration-github/src/lib/github/repository/dto/index.ts diff --git a/packages/plugins/integration-github/src/lib/github/repository/dto/update-github-repository.dto.ts b/packages/plugins/integration-github/src/lib/github/repository/dto/update-github-repository.dto.ts new file mode 100644 index 00000000000..58d4e958281 --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/repository/dto/update-github-repository.dto.ts @@ -0,0 +1,13 @@ +import { IOrganizationGithubRepositoryUpdateInput } from '@gauzy/contracts'; +import { IntersectionType, PickType } from '@nestjs/swagger'; +import { TenantOrganizationBaseDTO } from '@gauzy/core'; +import { OrganizationGithubRepository } from '../github-repository.entity'; + +/** + * A Data Transfer Object (DTO) for updating an organization's GitHub repository. + * This DTO is used to specify which properties of the repository should be updated. + * It combines properties from different sources to define the structure for the update. + */ +export class UpdateGithubRepositoryDTO + extends IntersectionType(TenantOrganizationBaseDTO, PickType(OrganizationGithubRepository, ['hasSyncEnabled'])) + implements IOrganizationGithubRepositoryUpdateInput {} diff --git a/packages/core/src/integration/github/repository/github-repository.controller.ts b/packages/plugins/integration-github/src/lib/github/repository/github-repository.controller.ts similarity index 54% rename from packages/core/src/integration/github/repository/github-repository.controller.ts rename to packages/plugins/integration-github/src/lib/github/repository/github-repository.controller.ts index 59359ce46fb..e8b48280382 100644 --- a/packages/core/src/integration/github/repository/github-repository.controller.ts +++ b/packages/plugins/integration-github/src/lib/github/repository/github-repository.controller.ts @@ -1,12 +1,12 @@ import { Body, Controller, Param, Post, Put } from '@nestjs/common'; -import { IIntegrationMapSyncRepository, IOrganizationGithubRepository } from '@gauzy/contracts'; -import { UUIDValidationPipe, UseValidationPipe } from '../../../shared/pipes'; +import { ID, IIntegrationMapSyncRepository, IOrganizationGithubRepository } from '@gauzy/contracts'; +import { UUIDValidationPipe, UseValidationPipe } from '@gauzy/core'; import { GithubRepositoryService } from './github-repository.service'; import { UpdateGithubRepositoryDTO } from './dto'; -@Controller('repository') +@Controller('/integration/github/repository') export class GitHubRepositoryController { - constructor(private readonly _githubRepositoryService: GithubRepositoryService) { } + constructor(private readonly _githubRepositoryService: GithubRepositoryService) {} /** * Sync a GitHub repository with Gauzy using provided data. @@ -16,12 +16,7 @@ export class GitHubRepositoryController { */ @Post('/sync') async syncRepository(@Body() entity: IIntegrationMapSyncRepository): Promise { - try { - return await this._githubRepositoryService.syncGithubRepository(entity); - } catch (error) { - // Handle errors, e.g., return an error response. - throw new Error('Failed to sync GitHub repository'); - } + return await this._githubRepositoryService.syncGithubRepository(entity); } /** @@ -33,21 +28,16 @@ export class GitHubRepositoryController { @Put('/:id') @UseValidationPipe({ whitelist: true }) async update( - @Param('id', UUIDValidationPipe) id: string, + @Param('id', UUIDValidationPipe) id: ID, @Body() input: UpdateGithubRepositoryDTO ): Promise { - try { - // Ensure that a GitHub repository with the provided identifier exists. - await this._githubRepositoryService.findOneByIdString(id); + // Ensure that a GitHub repository with the provided identifier exists. + await this._githubRepositoryService.findOneByIdString(id); - // Attempt to update the GitHub repository using the provided data. - return await this._githubRepositoryService.create({ - ...input, - id - }); - } catch (error) { - // Handle errors, e.g., return an error response. - throw new Error('Failed to update GitHub repository fields'); - } + // Attempt to update the GitHub repository using the provided data. + return await this._githubRepositoryService.create({ + ...input, + id + }); } } diff --git a/packages/plugins/integration-github/src/lib/github/repository/github-repository.entity.ts b/packages/plugins/integration-github/src/lib/github/repository/github-repository.entity.ts new file mode 100644 index 00000000000..35aa94ff6b2 --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/repository/github-repository.entity.ts @@ -0,0 +1,110 @@ +import { JoinColumn, RelationId } from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { + ID, + IIntegrationTenant, + IOrganizationGithubRepository, + IOrganizationGithubRepositoryIssue +} from '@gauzy/contracts'; +import { IntegrationTenant, TenantOrganizationBaseEntity } from '@gauzy/core'; +import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne, MultiORMOneToMany } from '@gauzy/core'; +import { MikroOrmOrganizationGithubRepositoryRepository } from './repository/mikro-orm-organization-github-repository.repository'; +import { OrganizationGithubRepositoryIssue } from './issue/github-repository-issue.entity'; + +@MultiORMEntity('organization_github_repository', { + mikroOrmRepository: () => MikroOrmOrganizationGithubRepositoryRepository +}) +export class OrganizationGithubRepository + extends TenantOrganizationBaseEntity + implements IOrganizationGithubRepository +{ + @ApiProperty({ type: () => Number }) + @IsNotEmpty() + @IsNumber() + @ColumnIndex() + @MultiORMColumn({ type: 'bigint' }) + repositoryId: number; + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsString() + @ColumnIndex() + @MultiORMColumn() + name: string; + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsString() + @ColumnIndex() + @MultiORMColumn() + fullName: string; + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsString() + @ColumnIndex() + @MultiORMColumn() + owner: string; + + @ApiPropertyOptional({ type: () => Number }) + @IsNotEmpty() + @IsNumber() + @ColumnIndex() + @MultiORMColumn({ nullable: true }) + issuesCount: number; + + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @ColumnIndex() + @MultiORMColumn({ nullable: true, default: true }) + hasSyncEnabled: boolean; + + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @ColumnIndex() + @MultiORMColumn({ nullable: true, default: false }) + private: boolean; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @ColumnIndex() + @MultiORMColumn({ nullable: true }) + status: string; + + /* + |-------------------------------------------------------------------------- + | @ManyToOne + |-------------------------------------------------------------------------- + */ + + /** What integration tenant sync to */ + @MultiORMManyToOne(() => IntegrationTenant, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true, + + /** Database cascade action on delete. */ + onDelete: 'CASCADE' + }) + @JoinColumn() + integration: IIntegrationTenant; + + @ApiProperty({ type: () => String }) + @IsUUID() + @RelationId((it: OrganizationGithubRepository) => it.integration) + @ColumnIndex() + @MultiORMColumn({ nullable: true, relationId: true }) + integrationId: ID; + + /* + |-------------------------------------------------------------------------- + | @OneToMany + |-------------------------------------------------------------------------- + */ + + /** Repository Sync Organization Projects */ + @MultiORMOneToMany(() => OrganizationGithubRepositoryIssue, (it) => it.repository, { cascade: true }) + issues?: IOrganizationGithubRepositoryIssue[]; +} diff --git a/packages/core/src/integration/github/repository/github-repository.service.ts b/packages/plugins/integration-github/src/lib/github/repository/github-repository.service.ts similarity index 74% rename from packages/core/src/integration/github/repository/github-repository.service.ts rename to packages/plugins/integration-github/src/lib/github/repository/github-repository.service.ts index b80d65fa308..e856544c421 100644 --- a/packages/core/src/integration/github/repository/github-repository.service.ts +++ b/packages/plugins/integration-github/src/lib/github/repository/github-repository.service.ts @@ -1,22 +1,22 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { CommandBus } from '@nestjs/cqrs'; import { IIntegrationMapSyncRepository, IOrganizationGithubRepository } from '@gauzy/contracts'; -import { TenantAwareCrudService } from 'core/crud'; +import { TenantAwareCrudService } from '@gauzy/core'; import { OrganizationGithubRepository } from './github-repository.entity'; import { IntegrationSyncGithubRepositoryCommand } from '../commands'; -import { MikroOrmOrganizationGithubRepositoryRepository } from './repository/mikro-orm-organization-github-repository.repository'; -import { TypeOrmOrganizationGithubRepositoryRepository } from './repository/type-orm-organization-github-repository.repository'; +import { + MikroOrmOrganizationGithubRepositoryRepository, + TypeOrmOrganizationGithubRepositoryRepository +} from './repository'; @Injectable() export class GithubRepositoryService extends TenantAwareCrudService { private readonly logger = new Logger('GithubRepositoryService'); constructor( + private readonly _commandBus: CommandBus, typeOrmOrganizationGithubRepositoryRepository: TypeOrmOrganizationGithubRepositoryRepository, - - mikroOrmOrganizationGithubRepositoryRepository: MikroOrmOrganizationGithubRepositoryRepository, - - private readonly _commandBus: CommandBus + mikroOrmOrganizationGithubRepositoryRepository: MikroOrmOrganizationGithubRepositoryRepository ) { super(typeOrmOrganizationGithubRepositoryRepository, mikroOrmOrganizationGithubRepositoryRepository); } @@ -29,9 +29,7 @@ export class GithubRepositoryService extends TenantAwareCrudService { try { - return await this._commandBus.execute( - new IntegrationSyncGithubRepositoryCommand(input) - ); + return await this._commandBus.execute(new IntegrationSyncGithubRepositoryCommand(input)); } catch (error) { // Handle errors and return an appropriate error response this.logger.error('Error while sync github integration repository', error.message); diff --git a/packages/core/src/integration/github/repository/issue/commands/handlers/index.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/commands/handlers/index.ts similarity index 100% rename from packages/core/src/integration/github/repository/issue/commands/handlers/index.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/commands/handlers/index.ts diff --git a/packages/core/src/integration/github/repository/issue/commands/handlers/integration-sync-github-repository-issue.handler.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/commands/handlers/integration-sync-github-repository-issue.handler.ts similarity index 84% rename from packages/core/src/integration/github/repository/issue/commands/handlers/integration-sync-github-repository-issue.handler.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/commands/handlers/integration-sync-github-repository-issue.handler.ts index 1e4ea3457bf..4faedab6adf 100644 --- a/packages/core/src/integration/github/repository/issue/commands/handlers/integration-sync-github-repository-issue.handler.ts +++ b/packages/plugins/integration-github/src/lib/github/repository/issue/commands/handlers/integration-sync-github-repository-issue.handler.ts @@ -1,18 +1,22 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { IOrganizationGithubRepository, IOrganizationGithubRepositoryFindInput, IOrganizationGithubRepositoryIssue } from '@gauzy/contracts'; -import { RequestContext } from '../../../../../../core/context'; +import { + IOrganizationGithubRepository, + IOrganizationGithubRepositoryFindInput, + IOrganizationGithubRepositoryIssue +} from '@gauzy/contracts'; +import { RequestContext } from '@gauzy/core'; import { IntegrationSyncGithubRepositoryIssueCommand } from '../integration-sync-github-repository-issue.command'; import { TypeOrmOrganizationGithubRepositoryRepository } from '../../../repository/type-orm-organization-github-repository.repository'; import { TypeOrmOrganizationGithubRepositoryIssueRepository } from '../../repository/type-orm-github-repository-issue.repository'; @CommandHandler(IntegrationSyncGithubRepositoryIssueCommand) -export class IntegrationSyncGithubRepositoryIssueCommandHandler implements ICommandHandler { - +export class IntegrationSyncGithubRepositoryIssueCommandHandler + implements ICommandHandler +{ constructor( private readonly typeOrmOrganizationGithubRepositoryRepository: TypeOrmOrganizationGithubRepositoryRepository, - - private readonly typeOrmOrganizationGithubRepositoryIssueRepository: TypeOrmOrganizationGithubRepositoryIssueRepository, - ) { } + private readonly typeOrmOrganizationGithubRepositoryIssueRepository: TypeOrmOrganizationGithubRepositoryIssueRepository + ) {} /** * Execute a command to synchronize a GitHub repository issue and store it in the local database. @@ -20,7 +24,9 @@ export class IntegrationSyncGithubRepositoryIssueCommandHandler implements IComm * @param command - The command containing input parameters for the synchronization. * @returns A Promise that resolves to the synchronized organization's GitHub repository issue. */ - public async execute(command: IntegrationSyncGithubRepositoryIssueCommand): Promise { + public async execute( + command: IntegrationSyncGithubRepositoryIssueCommand + ): Promise { try { // Extract input parameters from the command const { input, repositoryId, issue } = command; @@ -84,7 +90,7 @@ export class IntegrationSyncGithubRepositoryIssueCommandHandler implements IComm organizationId, tenantId, integrationId, - repositoryId, + repositoryId }); } } diff --git a/packages/core/src/integration/github/repository/issue/commands/index.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/commands/index.ts similarity index 100% rename from packages/core/src/integration/github/repository/issue/commands/index.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/commands/index.ts diff --git a/packages/core/src/integration/github/repository/issue/commands/integration-sync-github-repository-issue.command.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/commands/integration-sync-github-repository-issue.command.ts similarity index 90% rename from packages/core/src/integration/github/repository/issue/commands/integration-sync-github-repository-issue.command.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/commands/integration-sync-github-repository-issue.command.ts index 5a7abbd2183..58434d13215 100644 --- a/packages/core/src/integration/github/repository/issue/commands/integration-sync-github-repository-issue.command.ts +++ b/packages/plugins/integration-github/src/lib/github/repository/issue/commands/integration-sync-github-repository-issue.command.ts @@ -7,6 +7,6 @@ export class IntegrationSyncGithubRepositoryIssueCommand implements ICommand { constructor( public readonly input: IIntegrationMapSyncBase, public readonly repositoryId: IOrganizationGithubRepository['repositoryId'], - public readonly issue: IGithubIssue, - ) { } + public readonly issue: IGithubIssue + ) {} } diff --git a/packages/plugins/integration-github/src/lib/github/repository/issue/github-repository-issue.entity.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/github-repository-issue.entity.ts new file mode 100644 index 00000000000..0d92db31671 --- /dev/null +++ b/packages/plugins/integration-github/src/lib/github/repository/issue/github-repository-issue.entity.ts @@ -0,0 +1,57 @@ +import { JoinColumn, RelationId } from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { ID, IOrganizationGithubRepository, IOrganizationGithubRepositoryIssue } from '@gauzy/contracts'; +import { TenantOrganizationBaseEntity } from '@gauzy/core'; +import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from '@gauzy/core'; +import { OrganizationGithubRepository } from './../github-repository.entity'; +import { MikroOrmOrganizationGithubRepositoryIssueRepository } from './repository/mikro-orm-github-repository-issue.repository'; + +@MultiORMEntity('organization_github_repository_issue', { + mikroOrmRepository: () => MikroOrmOrganizationGithubRepositoryIssueRepository +}) +export class OrganizationGithubRepositoryIssue + extends TenantOrganizationBaseEntity + implements IOrganizationGithubRepositoryIssue +{ + @ApiProperty({ type: () => Number }) + @IsNotEmpty() + @IsNumber() + @ColumnIndex() + @MultiORMColumn({ type: 'bigint' }) + issueId: number; + + @ApiProperty({ type: () => Number }) + @IsNotEmpty() + @IsString() + @ColumnIndex() + @MultiORMColumn() + issueNumber: number; + + /* + |-------------------------------------------------------------------------- + | @ManyToOne + |-------------------------------------------------------------------------- + */ + + /** + * Organization Github Repository + */ + @MultiORMManyToOne(() => OrganizationGithubRepository, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true, + + /** Database cascade action on delete. */ + onDelete: 'SET NULL' + }) + @JoinColumn() + repository?: IOrganizationGithubRepository; + + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() + @RelationId((it: OrganizationGithubRepositoryIssue) => it.repository) + @ColumnIndex() + @MultiORMColumn({ nullable: true, relationId: true }) + repositoryId?: ID; +} diff --git a/packages/core/src/integration/github/repository/issue/github-repository-issue.service.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/github-repository-issue.service.ts similarity index 59% rename from packages/core/src/integration/github/repository/issue/github-repository-issue.service.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/github-repository-issue.service.ts index 4c1a7832d69..c50c812a838 100644 --- a/packages/core/src/integration/github/repository/issue/github-repository-issue.service.ts +++ b/packages/plugins/integration-github/src/lib/github/repository/issue/github-repository-issue.service.ts @@ -1,16 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { TenantAwareCrudService } from 'core/crud'; +import { TenantAwareCrudService } from '@gauzy/core'; import { OrganizationGithubRepositoryIssue } from './github-repository-issue.entity'; -import { TypeOrmOrganizationGithubRepositoryIssueRepository } from './repository/type-orm-github-repository-issue.repository'; -import { MikroOrmOrganizationGithubRepositoryIssueRepository } from './repository/mikro-orm-github-repository-issue.repository'; +import { + MikroOrmOrganizationGithubRepositoryIssueRepository, + TypeOrmOrganizationGithubRepositoryIssueRepository +} from './repository'; @Injectable() export class GithubRepositoryIssueService extends TenantAwareCrudService { constructor( - @InjectRepository(OrganizationGithubRepositoryIssue) typeOrmOrganizationGithubRepositoryIssueRepository: TypeOrmOrganizationGithubRepositoryIssueRepository, - mikroOrmOrganizationGithubRepositoryIssueRepository: MikroOrmOrganizationGithubRepositoryIssueRepository ) { super(typeOrmOrganizationGithubRepositoryIssueRepository, mikroOrmOrganizationGithubRepositoryIssueRepository); diff --git a/packages/core/src/integration/github/repository/issue/repository/index.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/repository/index.ts similarity index 100% rename from packages/core/src/integration/github/repository/issue/repository/index.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/repository/index.ts diff --git a/packages/core/src/integration/github/repository/issue/repository/mikro-orm-github-repository-issue.repository.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/repository/mikro-orm-github-repository-issue.repository.ts similarity index 63% rename from packages/core/src/integration/github/repository/issue/repository/mikro-orm-github-repository-issue.repository.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/repository/mikro-orm-github-repository-issue.repository.ts index fd04ff0aa69..d80efa1c88d 100644 --- a/packages/core/src/integration/github/repository/issue/repository/mikro-orm-github-repository-issue.repository.ts +++ b/packages/plugins/integration-github/src/lib/github/repository/issue/repository/mikro-orm-github-repository-issue.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../../../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { OrganizationGithubRepositoryIssue } from '../github-repository-issue.entity'; -export class MikroOrmOrganizationGithubRepositoryIssueRepository extends MikroOrmBaseEntityRepository { } +export class MikroOrmOrganizationGithubRepositoryIssueRepository extends MikroOrmBaseEntityRepository {} diff --git a/packages/core/src/integration/github/repository/issue/repository/type-orm-github-repository-issue.repository.ts b/packages/plugins/integration-github/src/lib/github/repository/issue/repository/type-orm-github-repository-issue.repository.ts similarity index 100% rename from packages/core/src/integration/github/repository/issue/repository/type-orm-github-repository-issue.repository.ts rename to packages/plugins/integration-github/src/lib/github/repository/issue/repository/type-orm-github-repository-issue.repository.ts diff --git a/packages/core/src/integration/github/repository/repository/index.ts b/packages/plugins/integration-github/src/lib/github/repository/repository/index.ts similarity index 100% rename from packages/core/src/integration/github/repository/repository/index.ts rename to packages/plugins/integration-github/src/lib/github/repository/repository/index.ts diff --git a/packages/core/src/integration/github/repository/repository/mikro-orm-organization-github-repository.repository.ts b/packages/plugins/integration-github/src/lib/github/repository/repository/mikro-orm-organization-github-repository.repository.ts similarity index 64% rename from packages/core/src/integration/github/repository/repository/mikro-orm-organization-github-repository.repository.ts rename to packages/plugins/integration-github/src/lib/github/repository/repository/mikro-orm-organization-github-repository.repository.ts index 742137a6890..93c3559d2ea 100644 --- a/packages/core/src/integration/github/repository/repository/mikro-orm-organization-github-repository.repository.ts +++ b/packages/plugins/integration-github/src/lib/github/repository/repository/mikro-orm-organization-github-repository.repository.ts @@ -1,4 +1,4 @@ -import { MikroOrmBaseEntityRepository } from '../../../../core/repository/mikro-orm-base-entity.repository'; +import { MikroOrmBaseEntityRepository } from '@gauzy/core'; import { OrganizationGithubRepository } from '../github-repository.entity'; -export class MikroOrmOrganizationGithubRepositoryRepository extends MikroOrmBaseEntityRepository { } +export class MikroOrmOrganizationGithubRepositoryRepository extends MikroOrmBaseEntityRepository {} diff --git a/packages/core/src/integration/github/repository/repository/type-orm-organization-github-repository.repository.ts b/packages/plugins/integration-github/src/lib/github/repository/repository/type-orm-organization-github-repository.repository.ts similarity index 100% rename from packages/core/src/integration/github/repository/repository/type-orm-organization-github-repository.repository.ts rename to packages/plugins/integration-github/src/lib/github/repository/repository/type-orm-organization-github-repository.repository.ts diff --git a/packages/plugins/integration-github/src/lib/integration-github.plugin.ts b/packages/plugins/integration-github/src/lib/integration-github.plugin.ts new file mode 100644 index 00000000000..598ed9f4d6f --- /dev/null +++ b/packages/plugins/integration-github/src/lib/integration-github.plugin.ts @@ -0,0 +1,51 @@ +import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin'; +import { ApplicationPluginConfig } from '@gauzy/common'; +import { GithubModule } from './github/github.module'; +import { OrganizationGithubRepository } from './github/repository/github-repository.entity'; +import { OrganizationGithubRepositoryIssue } from './github/repository/issue/github-repository-issue.entity'; + +@Plugin({ + imports: [GithubModule], + entities: [OrganizationGithubRepository, OrganizationGithubRepositoryIssue], + configuration: (config: ApplicationPluginConfig) => { + config.customFields.OrganizationProject.push({ + name: 'repository', + type: 'relation', + relationType: 'many-to-one', + entity: OrganizationGithubRepository, + nullable: true, // Determines whether the relation is nullable. + onDelete: 'SET NULL' // Defines the database cascade action on delete. + }); + config.customFields.OrganizationProject.push({ + name: 'repositoryId', + type: 'string', + relation: 'repository', + nullable: true, // Determines whether the relation is nullable. + relationId: true, + index: true + }); + return config; + } +}) +export class IntegrationGithubPlugin implements IOnPluginBootstrap, IOnPluginDestroy { + // We disable by default additional logging for each event to avoid cluttering the logs + private logEnabled = true; + + /** + * Called when the plugin is being initialized. + */ + onPluginBootstrap(): void | Promise { + if (this.logEnabled) { + console.log(`${IntegrationGithubPlugin.name} is being bootstrapped...`); + } + } + + /** + * Called when the plugin is being destroyed. + */ + onPluginDestroy(): void | Promise { + if (this.logEnabled) { + console.log(`${IntegrationGithubPlugin.name} is being destroyed...`); + } + } +} diff --git a/packages/plugins/integration-github/src/hook-metadata.accessor.ts b/packages/plugins/integration-github/src/lib/probot/hook-metadata.accessor.ts similarity index 92% rename from packages/plugins/integration-github/src/hook-metadata.accessor.ts rename to packages/plugins/integration-github/src/lib/probot/hook-metadata.accessor.ts index 970e8538624..614b8d6331f 100644 --- a/packages/plugins/integration-github/src/hook-metadata.accessor.ts +++ b/packages/plugins/integration-github/src/lib/probot/hook-metadata.accessor.ts @@ -4,7 +4,7 @@ import { EmitterWebhookEventName } from '@octokit/webhooks/dist-types/types'; @Injectable() export class HookMetadataAccessor { - constructor(private readonly reflector: Reflector) { } + constructor(private readonly reflector: Reflector) {} /** * Get the webhook events associated with a target. diff --git a/packages/plugins/integration-github/src/hook.controller.ts b/packages/plugins/integration-github/src/lib/probot/hook.controller.ts similarity index 100% rename from packages/plugins/integration-github/src/hook.controller.ts rename to packages/plugins/integration-github/src/lib/probot/hook.controller.ts diff --git a/packages/plugins/integration-github/src/hook.decorator.ts b/packages/plugins/integration-github/src/lib/probot/hook.decorator.ts similarity index 75% rename from packages/plugins/integration-github/src/hook.decorator.ts rename to packages/plugins/integration-github/src/lib/probot/hook.decorator.ts index b104ae3fe20..a455dad6e23 100644 --- a/packages/plugins/integration-github/src/hook.decorator.ts +++ b/packages/plugins/integration-github/src/lib/probot/hook.decorator.ts @@ -5,8 +5,6 @@ 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 { +export function Hook(eventOrEvents: EmitterWebhookEventName | EmitterWebhookEventName[]): MethodDecorator { return applyDecorators(SetMetadata('HOOK_EVENTS', { eventOrEvents })); } diff --git a/packages/plugins/integration-github/src/lib/probot/index.ts b/packages/plugins/integration-github/src/lib/probot/index.ts new file mode 100644 index 00000000000..8f4d0549d2e --- /dev/null +++ b/packages/plugins/integration-github/src/lib/probot/index.ts @@ -0,0 +1,6 @@ +export * from './hook.decorator'; +export * from './octokit.service'; +export * from './probot.discovery'; +export * from './probot.helpers'; +export * from './probot.module'; +export * from './probot.types'; diff --git a/packages/plugins/integration-github/src/octokit.service.ts b/packages/plugins/integration-github/src/lib/probot/octokit.service.ts similarity index 88% rename from packages/plugins/integration-github/src/octokit.service.ts rename to packages/plugins/integration-github/src/lib/probot/octokit.service.ts index e30291a6179..9a4dc58357c 100644 --- a/packages/plugins/integration-github/src/octokit.service.ts +++ b/packages/plugins/integration-github/src/lib/probot/octokit.service.ts @@ -31,7 +31,7 @@ export class OctokitService { appId: config.appId, privateKey: config.privateKey, clientId: config.clientId, - clientSecret: config.clientSecret, + clientSecret: config.clientSecret }); // console.log(chalk.magenta(`Octokit App Configuration ${JSON.stringify(config)}`)); console.log(chalk.green(`Octokit App successfully initialized.`)); @@ -73,8 +73,8 @@ export class OctokitService { return await octokit.request(endpoint, { installationId, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }); } catch (error) { this.logger.error('Failed to fetch GitHub installation metadata', error.message); @@ -106,8 +106,8 @@ export class OctokitService { return await octokit.request(endpoint, { installationId, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }); } catch (error) { // Handle errors, log the error message, and throw a new error @@ -123,9 +123,7 @@ export class OctokitService { * @returns {Promise>} A promise that resolves with the GitHub repositories. * @throws {Error} If the request to fetch repositories fails. */ - public async getRepositories( - installationId: number - ): Promise> { + public async getRepositories(installationId: number): Promise> { if (!this.app) { throw new Error('Octokit instance is not available.'); } @@ -140,8 +138,8 @@ export class OctokitService { return await octokit.request(endpoint, { installationId, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }); } catch (error) { this.logger.error('Failed to fetch GitHub installation repositories', error.message); @@ -159,12 +157,10 @@ export class OctokitService { * @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 getRepositoryIssues(installationId: number, { - owner, - repo, - page = 1, - per_page = 100 - }): Promise> { + public async getRepositoryIssues( + installationId: number, + { owner, repo, page = 1, per_page = 100 } + ): Promise> { if (!this.app) { throw new Error('Octokit instance is not available.'); } @@ -182,8 +178,8 @@ export class OctokitService { page, per_page, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }); } catch (error) { this.logger.error('Failed to fetch GitHub installation repository issues', error.message); @@ -204,15 +200,18 @@ export class OctokitService { * @returns A promise that resolves to the response from the GitHub API containing labels associated with the issue. * @throws {Error} If the request to the GitHub API fails or if the Octokit instance is unavailable. */ - public async getLabelsByIssueNumber(installationId: number, { - owner, - repo, - issue_number - }: { - owner: string; - repo: string; - issue_number: number; - }): Promise> { + public async getLabelsByIssueNumber( + installationId: number, + { + owner, + repo, + issue_number + }: { + owner: string; + repo: string; + issue_number: number; + } + ): Promise> { if (!this.app) { throw new Error('Octokit instance is not available.'); } @@ -228,8 +227,8 @@ export class OctokitService { repo, issue_number, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }); } catch (error) { this.logger.error('Failed to fetch GitHub issue labels', error.message); @@ -249,17 +248,20 @@ export class OctokitService { * @returns A promise that resolves to an OctokitResponse. * @throws An error if Octokit instance is not available or if the request fails. */ - public async addLabelsForIssue(installationId: number, { - owner, - repo, - issue_number, - labels - }: { - owner: string; - repo: string; - issue_number: number; - labels: string[]; - }): Promise> { + public async addLabelsForIssue( + installationId: number, + { + owner, + repo, + issue_number, + labels + }: { + owner: string; + repo: string; + issue_number: number; + labels: string[]; + } + ): Promise> { if (!this.app) { throw new Error('Octokit instance is not available.'); } @@ -277,8 +279,8 @@ export class OctokitService { issue_number, labels, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }); } catch (error) { // Handle any errors that occur during the process @@ -298,15 +300,18 @@ export class OctokitService { * @returns A promise that resolves to the response from the GitHub API. * @throws If the request to the GitHub API fails. */ - public async getIssueByIssueNumber(installationId: number, { - owner, - repo, - issue_number - }: { - owner: string; - repo: string; - issue_number: number; - }): Promise> { + public async getIssueByIssueNumber( + installationId: number, + { + owner, + repo, + issue_number + }: { + owner: string; + repo: string; + issue_number: number; + } + ): Promise> { if (!this.app) { throw new Error('Octokit instance is not available.'); } @@ -322,8 +327,8 @@ export class OctokitService { repo, issue_number, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }); } catch (error) { this.logger.error('Failed to fetch GitHub repository issue', error.message); @@ -342,13 +347,10 @@ export class OctokitService { * @param labels - An array of labels for the issue. * @returns A promise that resolves to the response from GitHub. */ - public async openIssue(installationId: number, { - repo, - owner, - title, - body, - labels, - }): Promise> { + public async openIssue( + installationId: number, + { repo, owner, title, body, labels } + ): Promise> { if (!this.app) { throw new Error('Octokit instance is not available.'); } @@ -367,8 +369,8 @@ export class OctokitService { body, labels, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }; // Send the request to create the issue @@ -391,13 +393,11 @@ export class OctokitService { * @param labels - An array of updated labels for the issue. * @returns A promise that resolves to the response from GitHub. */ - public async updateIssue(installationId: number, issue_number: number, { - repo, - owner, - title, - body, - labels, - }): Promise> { + public async updateIssue( + installationId: number, + issue_number: number, + { repo, owner, title, body, labels } + ): Promise> { try { // Ensure that the Octokit instance is available if (!this.app) { @@ -419,8 +419,8 @@ export class OctokitService { body, labels, headers: { - 'X-GitHub-Api-Version': GITHUB_API_VERSION, - }, + 'X-GitHub-Api-Version': GITHUB_API_VERSION + } }; // Send the request to update the issue diff --git a/packages/plugins/integration-github/src/probot.discovery.ts b/packages/plugins/integration-github/src/lib/probot/probot.discovery.ts similarity index 89% rename from packages/plugins/integration-github/src/probot.discovery.ts rename to packages/plugins/integration-github/src/lib/probot/probot.discovery.ts index 9477168c7ac..5ddc25fd245 100644 --- a/packages/plugins/integration-github/src/probot.discovery.ts +++ b/packages/plugins/integration-github/src/lib/probot/probot.discovery.ts @@ -6,11 +6,11 @@ import { Logger, OnApplicationBootstrap, OnApplicationShutdown, - OnModuleInit, + OnModuleInit } from '@nestjs/common'; import { Probot } from 'probot'; import SmeeClient from 'smee-client'; -import * as _ from 'underscore'; +import { isEmpty } from 'underscore'; import * as chalk from 'chalk'; import { v4 } from 'uuid'; import { ModuleProviders, ProbotConfig } from './probot.types'; @@ -19,11 +19,10 @@ 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; + private smee: SmeeClient; constructor( private readonly discoveryService: DiscoveryService, @@ -47,7 +46,8 @@ export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, On } /** - * + * Called automatically when the module has been initialized. + * It discovers and initializes instance wrappers used within the module. */ public async onModuleInit() { this.discoverInstanceWrappers(); @@ -60,7 +60,7 @@ export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, On */ onApplicationBootstrap(): any { // Check if webhookProxy is configured - if (!_.isEmpty(this.config.webhookProxy)) { + if (!isEmpty(this.config.webhookProxy)) { // Create and start a SmeeClient if webhookProxy is configured this.smee = createSmee(this.config); this.smee.start(); @@ -88,13 +88,11 @@ export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, On return; } this.probot - .load((app: { - on: (eventName: any, callback: (context: any) => Promise) => any; - }) => { + .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 + hook.eventOrEvents, // The event name or names to listen for this.initContext(hook.target) // The callback function for the event ); }); @@ -124,12 +122,12 @@ export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, On // Get all instance wrappers for controllers and providers const instanceWrappers: InstanceWrapper[] = [ ...this.discoveryService.getControllers(), - ...this.discoveryService.getProviders(), + ...this.discoveryService.getProviders() ]; // Filter instance wrappers with static dependency trees - const staticInstanceWrappers = instanceWrappers.filter( - (wrapper: InstanceWrapper) => wrapper.isDependencyTreeStatic() + const staticInstanceWrappers = instanceWrappers.filter((wrapper: InstanceWrapper) => + wrapper.isDependencyTreeStatic() ); // Iterate through static instance wrappers and explore methods @@ -169,14 +167,14 @@ export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, On const hookFn = this.wrapFunctionInTryCatchBlocks(methodRef, instance); // If no webhook event definition, skip - if (_.isEmpty(hookMetadata)) { + if (isEmpty(hookMetadata)) { return null; } // Generate a unique key and store the hook information return this.hooks.set(v4(), { target: hookFn, - eventOrEvents: hookMetadata, + eventOrEvents: hookMetadata }); } @@ -186,10 +184,7 @@ export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, On * @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 - ) { + private wrapFunctionInTryCatchBlocks(methodRef: () => any, instance: Record) { // Return an asynchronous function that wraps the method reference return async (...args: unknown[]) => { try { @@ -211,6 +206,7 @@ export class ProbotDiscovery implements OnModuleInit, OnApplicationBootstrap, On 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']; diff --git a/packages/plugins/integration-github/src/probot.helpers.ts b/packages/plugins/integration-github/src/lib/probot/probot.helpers.ts similarity index 88% rename from packages/plugins/integration-github/src/probot.helpers.ts rename to packages/plugins/integration-github/src/lib/probot/probot.helpers.ts index d5c8abd4994..8ac1050b52f 100644 --- a/packages/plugins/integration-github/src/probot.helpers.ts +++ b/packages/plugins/integration-github/src/lib/probot/probot.helpers.ts @@ -16,7 +16,9 @@ export const GITHUB_API_URL = 'https://api.github.com'; */ export const parseConfig = (config: ProbotConfig): Record => ({ appId: config.appId, - privateKey: getPrivateKey({ env: { PRIVATE_KEY: config.privateKey ? config.privateKey.replace(/\\n/g, '\n') : '' } }) as string, + 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, @@ -40,7 +42,7 @@ export const createProbot = (config: ProbotConfig): Probot => { } return new Probot({ - ...parsedConfig, // Spread the parsed configuration properties + ...parsedConfig // Spread the parsed configuration properties }); }; @@ -54,7 +56,7 @@ export const createSmee = (config: ProbotConfig): SmeeClient => { return new SmeeClient({ source: parsedConfig.webhookProxy as string, target: parsedConfig.webhookPath as string, - logger: console, + logger: console }); }; @@ -75,7 +77,7 @@ export const createOctokit = (config: OctokitConfig): Octokit => { privateKey: probot.privateKey, clientId: probot.clientId, clientSecret: probot.clientSecret, - ...config.auth, // Include other auth options if needed - }, + ...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/lib/probot/probot.module.ts similarity index 67% rename from packages/plugins/integration-github/src/probot.module.ts rename to packages/plugins/integration-github/src/lib/probot/probot.module.ts index 9669a05b967..693e72a56b1 100644 --- a/packages/plugins/integration-github/src/probot.module.ts +++ b/packages/plugins/integration-github/src/lib/probot/probot.module.ts @@ -1,10 +1,6 @@ import { DiscoveryModule } from '@nestjs/core'; import { DynamicModule, Module } from '@nestjs/common'; -import { - ProbotModuleOptions, - ModuleProviders, - ProbotModuleAsyncOptions, -} from './probot.types'; +import { ProbotModuleOptions, ModuleProviders, ProbotModuleAsyncOptions } from './probot.types'; import { ProbotDiscovery } from './probot.discovery'; import { getControllerClass } from './hook.controller'; import { HookMetadataAccessor } from './hook-metadata.accessor'; @@ -16,35 +12,45 @@ import { OctokitService } from './octokit.service'; export class ProbotModule { /** * Register the Probot module. + * This function sets up and returns a dynamic module configuration for the Probot module. + * * @param options - Configuration options for the Probot module. * @returns A dynamic module configuration. */ static forRoot(options: ProbotModuleOptions): DynamicModule { + // Dynamically create a controller class based on the provided path option const HookController = getControllerClass({ path: options.path }); + + // Return the dynamic module configuration return { - global: options.isGlobal || true, module: ProbotModule, + global: options.isGlobal || true, controllers: [HookController], providers: [ { provide: ModuleProviders.ProbotConfig, - useFactory: () => options.config, + useFactory: () => options.config }, HookMetadataAccessor, ProbotDiscovery, - OctokitService, + OctokitService ], - exports: [OctokitService], + exports: [OctokitService] }; } /** * Register the Probot module asynchronously. + * This function sets up and returns a dynamic module configuration for the Probot module, asynchronously. + * * @param options - Configuration options for the Probot module. * @returns A dynamic module configuration. */ static forRootAsync(options: ProbotModuleAsyncOptions): DynamicModule { + // Dynamically create a controller class based on the provided path option const HookController = getControllerClass({ path: options.path }); + + // Return the dynamic module configuration return { module: ProbotModule, global: options.isGlobal || true, @@ -53,13 +59,13 @@ export class ProbotModule { { provide: ModuleProviders.ProbotConfig, useFactory: options.useFactory, - inject: options.inject || [], + inject: options.inject || [] }, HookMetadataAccessor, ProbotDiscovery, - OctokitService, + OctokitService ], - exports: [OctokitService], + exports: [OctokitService] }; } } diff --git a/packages/plugins/integration-github/src/probot.types.ts b/packages/plugins/integration-github/src/lib/probot/probot.types.ts similarity index 94% rename from packages/plugins/integration-github/src/probot.types.ts rename to packages/plugins/integration-github/src/lib/probot/probot.types.ts index f1f528d447d..02ca5326a9a 100644 --- a/packages/plugins/integration-github/src/probot.types.ts +++ b/packages/plugins/integration-github/src/lib/probot/probot.types.ts @@ -44,10 +44,10 @@ export interface ProbotModuleAsyncOptions extends Pick/tsconfig.spec.json' }] + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/packages/plugins/integration-hubstaff' +}; diff --git a/packages/plugins/integration-hubstaff/package.json b/packages/plugins/integration-hubstaff/package.json index cae7f14ae50..13bf96d5acc 100644 --- a/packages/plugins/integration-hubstaff/package.json +++ b/packages/plugins/integration-hubstaff/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/integration-hubstaff", + "name": "@gauzy/plugin-integration-hubstaff", "version": "0.1.0", "description": "Ever Gauzy Platform plugin for integration with HubStaff APIs", "author": { @@ -16,6 +16,23 @@ "url": "https://github.com/ever-co/ever-gauzy/issues" }, "homepage": "https://ever.co", + "keywords": [ + "Hubstaff", + "integration", + "time tracking", + "productivity", + "API", + "plugin", + "NestJS", + "Gauzy", + "Ever Gauzy", + "platform", + "management", + "tool", + "software", + "development", + "typescript" + ], "private": true, "main": "dist/index.js", "types": "dist/index.d.ts", @@ -30,17 +47,39 @@ "access": "restricted" }, "scripts": { - "test:e2e": "jest --config ./jest.config.js", - "build": "rimraf dist && yarn run compile", - "compile": "tsc -p tsconfig.lib.json" + "test:e2e": "jest --config ./jest.config.ts", + "build": "rimraf dist && tsc -p tsconfig.lib.json", + "build:prod": "rimraf dist && tsc -p tsconfig.lib.prod.json" + }, + "peerDependencies": { + "tslib": "^2.6.2" }, - "keywords": [], "dependencies": { - "@gauzy/contracts": "^0.1.0" + "@gauzy/common": "^0.1.0", + "@gauzy/config": "^0.1.0", + "@gauzy/contracts": "^0.1.0", + "@gauzy/core": "^0.1.0", + "@gauzy/plugin": "^0.1.0", + "@nestjs/axios": "^3.0.2", + "@nestjs/common": "^10.3.7", + "@nestjs/cqrs": "^10.2.7", + "@nestjs/swagger": "^7.3.0", + "axios": "^1.6.8", + "express": "^4.18.2", + "moment": "^2.30.1", + "rxjs": "^7.4.0", + "typeorm": "^0.3.20" }, "devDependencies": { + "@types/express": "^4.17.13", + "@types/jest": "^29.4.4", "@types/node": "^20.14.9", "rimraf": "^3.0.2", "typescript": "5.1.6" - } + }, + "engines": { + "node": ">=20.11.1", + "yarn": ">=1.22.19" + }, + "sideEffects": false } diff --git a/packages/plugins/integration-hubstaff/project.json b/packages/plugins/integration-hubstaff/project.json new file mode 100644 index 00000000000..0dc37033191 --- /dev/null +++ b/packages/plugins/integration-hubstaff/project.json @@ -0,0 +1,48 @@ +{ + "name": "plugin-integration-hubstaff", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugins/integration-hubstaff/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "./packages/plugins/integration-hubstaff/dist", + "tsConfig": "packages/plugins/integration-hubstaff/tsconfig.lib.json", + "packageJson": "packages/plugins/integration-hubstaff/package.json", + "main": "packages/plugins/integration-hubstaff/src/index.ts", + "assets": ["packages/plugins/integration-hubstaff/*.md"] + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "node tools/scripts/publish.mjs integration-hubstaff {args.ver} {args.tag}" + }, + "dependsOn": ["build"] + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/plugins/integration-hubstaff/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/plugins/integration-hubstaff/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + }, + "tags": [] +} diff --git a/packages/plugins/integration-hubstaff/src/hubstaff-entity-settings.ts b/packages/plugins/integration-hubstaff/src/hubstaff-entity-settings.ts deleted file mode 100644 index 9e64063c31b..00000000000 --- a/packages/plugins/integration-hubstaff/src/hubstaff-entity-settings.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { IntegrationEntity } from '@gauzy/contracts'; - -export const DEFAULT_ENTITY_SETTINGS = [ - { - entity: IntegrationEntity.ORGANIZATION, - sync: true - }, - { - entity: IntegrationEntity.PROJECT, - sync: true - }, - { - entity: IntegrationEntity.CLIENT, - sync: true - } -]; - -export const PROJECT_TIED_ENTITIES = [ - { - entity: IntegrationEntity.TASK, - sync: true - }, - { - entity: IntegrationEntity.ACTIVITY, - sync: true - }, - { - entity: IntegrationEntity.SCREENSHOT, - sync: true - } -]; diff --git a/packages/plugins/integration-hubstaff/src/index.ts b/packages/plugins/integration-hubstaff/src/index.ts index 1c1bc2f8426..1eeb201f98a 100644 --- a/packages/plugins/integration-hubstaff/src/index.ts +++ b/packages/plugins/integration-hubstaff/src/index.ts @@ -1,2 +1,2 @@ -export * from './hubstaff-entity-settings'; -export * from './hubstaff.config'; +export * from './lib/integration-hubstaff.plugin'; +export * from './lib/hubstaff.config'; diff --git a/packages/plugins/integration-hubstaff/src/lib/hubstaff-authorization.controller.ts b/packages/plugins/integration-hubstaff/src/lib/hubstaff-authorization.controller.ts new file mode 100644 index 00000000000..faa4798d69c --- /dev/null +++ b/packages/plugins/integration-hubstaff/src/lib/hubstaff-authorization.controller.ts @@ -0,0 +1,50 @@ +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 { IntegrationEnum } from '@gauzy/contracts'; +import { IHubstaffConfig, Public, createQueryParamsString } from '@gauzy/common'; + +@ApiTags('Hubstaff Integrations') +@Public() +@Controller('/integration/hubstaff') +export class HubstaffAuthorizationController { + constructor(private readonly _config: ConfigService) {} + + /** + * 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 hubstaffIntegrationCallback(@Query() query: any, @Res() response: Response) { + try { + // 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); + } + + /** Hubstaff Config Options */ + const hubstaff = this._config.get('hubstaff') as IHubstaffConfig; + + // Convert query params object to string + const queryParamsString = createQueryParamsString({ + code: query.code, + state: query.state + }); + + // Combine hubstaff post install URL with query params + const url = [hubstaff.postInstallUrl, queryParamsString].filter(Boolean).join('?'); + + /** Redirect to the URL */ + return response.redirect(url); + } catch (error) { + // 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/plugins/integration-hubstaff/src/hubstaff.config.ts b/packages/plugins/integration-hubstaff/src/lib/hubstaff.config.ts similarity index 100% rename from packages/plugins/integration-hubstaff/src/hubstaff.config.ts rename to packages/plugins/integration-hubstaff/src/lib/hubstaff.config.ts diff --git a/packages/plugins/integration-hubstaff/src/lib/hubstaff.controller.ts b/packages/plugins/integration-hubstaff/src/lib/hubstaff.controller.ts new file mode 100644 index 00000000000..59830abb68e --- /dev/null +++ b/packages/plugins/integration-hubstaff/src/lib/hubstaff.controller.ts @@ -0,0 +1,125 @@ +import { Controller, Post, Body, Get, Param, UseGuards, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { + IIntegrationTenant, + IHubstaffOrganization, + IHubstaffProject, + IIntegrationMap, + IIntegrationSetting, + PermissionsEnum, + ICreateHubstaffIntegrationInput, + ID +} from '@gauzy/contracts'; +import { PermissionGuard, Permissions, TenantPermissionGuard, UUIDValidationPipe } from '@gauzy/core'; +import { HubstaffService } from './hubstaff.service'; + +@ApiTags('Hubstaff Integrations') +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Permissions(PermissionsEnum.INTEGRATION_VIEW) +@Controller('/integration/hubstaff') +export class HubstaffController { + constructor(private readonly _hubstaffService: HubstaffService) {} + + /** + * Get Hubstaff token by integration ID + * + * @param integrationId The ID of the integration + * @returns Integration setting containing the Hubstaff token + */ + @Get('/token/:integrationId') + async getHubstaffTokenByIntegration( + @Param('integrationId', UUIDValidationPipe) integrationId: ID + ): Promise { + return await this._hubstaffService.getHubstaffToken(integrationId); + } + + /** + * Refresh Hubstaff token by integration ID + * + * @param integrationId The ID of the integration + * @returns The refreshed Hubstaff token + */ + @Get('/refresh-token/:integrationId') + async refreshHubstaffTokenByIntegration( + @Param('integrationId', UUIDValidationPipe) integrationId: ID + ): Promise { + return await this._hubstaffService.refreshToken(integrationId); + } + + /** + * Create a new Hubstaff integration + * + * @param body The input data for creating the integration + * @returns The created integration tenant + */ + @Post('/integration') + async create(@Body() body: ICreateHubstaffIntegrationInput): Promise { + return await this._hubstaffService.addIntegration(body); + } + + /** + * Get organizations from Hubstaff + * + * @param token The authentication token + * @returns List of Hubstaff organizations + */ + @Get('/organizations') + async getOrganizations(@Query('token') token: string): Promise { + return await this._hubstaffService.fetchOrganizations(token); + } + + /** + * Get projects for a specific organization from Hubstaff + * + * @param organizationId The ID of the organization + * @param token The authentication token + * @returns List of projects for the organization + */ + @Get('/projects/:organizationId') + async getProjects( + @Param('organizationId') organizationId: ID, + @Query('token') token: string + ): Promise { + return await this._hubstaffService.fetchOrganizationProjects({ + token, + organizationId + }); + } + + /** + * Sync projects data with Hubstaff + * + * @param input The input data for syncing projects + * @returns List of integration maps after syncing + */ + @Post('/sync-projects') + async syncProjects(@Body() input: any): Promise { + return await this._hubstaffService.syncProjects(input); + } + + /** + * Sync organizations data with Hubstaff + * + * @param input The input data for syncing organizations + * @returns List of integration maps after syncing + */ + @Post('/sync-organizations') + async syncOrganizations(@Body() input: any): Promise { + return await this._hubstaffService.syncOrganizations(input); + } + + /** + * Automatically sync data for an integration with Hubstaff + * + * @param integrationId The ID of the integration + * @param body The input data for auto-sync + * @returns Result of the auto-sync operation + */ + @Post('/auto-sync/:integrationId') + async autoSync(@Param('integrationId', UUIDValidationPipe) integrationId: ID, @Body() body): Promise { + return await this._hubstaffService.autoSync({ + ...body, + integrationId + }); + } +} diff --git a/packages/plugins/integration-hubstaff/src/lib/hubstaff.module.ts b/packages/plugins/integration-hubstaff/src/lib/hubstaff.module.ts new file mode 100644 index 00000000000..b49a8cf21b0 --- /dev/null +++ b/packages/plugins/integration-hubstaff/src/lib/hubstaff.module.ts @@ -0,0 +1,43 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; +import { + IntegrationEntitySettingModule, + IntegrationEntitySettingTiedModule, + IntegrationMapModule, + IntegrationModule, + IntegrationSettingModule, + IntegrationTenantModule, + OrganizationModule, + OrganizationProjectModule, + RoleModule, + RolePermissionModule, + ScreenshotModule, + UserModule +} from '@gauzy/core'; +import { HUBSTAFF_API_URL } from './hubstaff.config'; +import { HubstaffService } from './hubstaff.service'; +import { HubstaffController } from './hubstaff.controller'; +import { HubstaffAuthorizationController } from './hubstaff-authorization.controller'; + +@Module({ + imports: [ + HttpModule.register({ baseURL: HUBSTAFF_API_URL }), + CqrsModule, + IntegrationEntitySettingModule, + IntegrationEntitySettingTiedModule, + IntegrationMapModule, + IntegrationModule, + IntegrationSettingModule, + IntegrationTenantModule, + OrganizationModule, + OrganizationProjectModule, + RoleModule, + RolePermissionModule, + ScreenshotModule, + UserModule + ], + controllers: [HubstaffAuthorizationController, HubstaffController], + providers: [HubstaffService] +}) +export class HubstaffModule {} diff --git a/packages/plugins/integration-hubstaff/src/lib/hubstaff.service.ts b/packages/plugins/integration-hubstaff/src/lib/hubstaff.service.ts new file mode 100644 index 00000000000..8aae104bed6 --- /dev/null +++ b/packages/plugins/integration-hubstaff/src/lib/hubstaff.service.ts @@ -0,0 +1,1625 @@ +import { Injectable, BadRequestException, HttpException, HttpStatus, NotFoundException } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { CommandBus } from '@nestjs/cqrs'; +import { AxiosError, AxiosResponse } from 'axios'; +import { DeepPartial } from 'typeorm'; +import { firstValueFrom, lastValueFrom } from 'rxjs'; +import { map, catchError, switchMap } from 'rxjs/operators'; +import * as moment from 'moment'; +import { environment as env } from '@gauzy/config'; +import { isEmpty, isNotEmpty, isObject } from '@gauzy/common'; +import { + IIntegrationTenant, + IntegrationEnum, + IntegrationEntity, + IIntegrationMap, + IIntegrationSetting, + RolesEnum, + TimeLogType, + ContactType, + CurrenciesEnum, + ProjectBillingEnum, + TimeLogSourceEnum, + IHubstaffOrganization, + IHubstaffProject, + IIntegrationEntitySetting, + IDateRangeActivityFilter, + ComponentLayoutStyleEnum, + ActivityType, + IDateRange, + OrganizationProjectBudgetTypeEnum, + OrganizationContactBudgetTypeEnum, + IHubstaffProjectsResponse, + IHubstaffOrganizationsResponse, + IHubstaffProjectResponse, + IHubstaffTimeSlotActivity, + IActivity, + IHubstaffLogFromTimeSlots, + ICreateHubstaffIntegrationInput, + ID +} from '@gauzy/contracts'; +import { DEFAULT_ENTITY_SETTINGS, PROJECT_TIED_ENTITIES, mergeOverlappingDateRanges } from '@gauzy/core'; +import { + IntegrationMapService, + IntegrationService, + IntegrationSettingService, + IntegrationTenantService, + OrganizationService, + RequestContext, + RoleService, + UserService +} from '@gauzy/core'; +import { + EmployeeCreateCommand, + EmployeeGetCommand, + IntegrationMapSyncActivityCommand, + IntegrationMapSyncEntityCommand, + IntegrationMapSyncOrganizationCommand, + IntegrationMapSyncProjectCommand, + IntegrationMapSyncScreenshotCommand, + IntegrationMapSyncTaskCommand, + IntegrationMapSyncTimeLogCommand, + IntegrationMapSyncTimeSlotCommand, + IntegrationTenantUpdateOrCreateCommand, + OrganizationContactCreateCommand +} from '@gauzy/core'; +import { HUBSTAFF_AUTHORIZATION_URL } from './hubstaff.config'; + +@Injectable() +export class HubstaffService { + constructor( + private readonly _httpService: HttpService, + private readonly _commandBus: CommandBus, + private readonly _integrationTenantService: IntegrationTenantService, + private readonly _integrationSettingService: IntegrationSettingService, + private readonly _integrationMapService: IntegrationMapService, + private readonly _roleService: RoleService, + private readonly _organizationService: OrganizationService, + private readonly _userService: UserService, + private readonly _integrationService: IntegrationService + ) {} + + /** + * Fetch data from an external integration API using HTTP GET request. + * + * @param {string} url - The URL to fetch data from. + * @param {string} token - Bearer token for authorization. + * @returns {Promise} - A promise resolving to the fetched data. + */ + async fetchIntegration(url: string, token: string): Promise { + const headers = { + Authorization: `Bearer ${token}` + }; + return firstValueFrom( + this._httpService.get(url, { headers }).pipe( + catchError((error: AxiosError) => { + const response: AxiosResponse = error.response; + console.log('Error while hubstaff API: %s', response); + + /** Handle hubstaff http exception */ + throw new HttpException({ message: error.message, error }, response.status); + }), + map((response: AxiosResponse) => response.data) + ) + ); + } + + /** + * Refresh the access token for the specified integration. + * + * @param integrationId The ID of the integration. + * @returns The new tokens. + */ + async refreshToken(integrationId: ID) { + const settings = await this._integrationSettingService.find({ + where: { + integration: { id: integrationId }, + integrationId + } + }); + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + const urlParams = new URLSearchParams(); + + const { client_id, client_secret, refresh_token } = settings.reduce( + (prev, current) => { + return { + ...prev, + client_id: current.settingsName === 'client_id' ? current.settingsValue : prev.client_id, + client_secret: + current.settingsName === 'client_secret' ? current.settingsValue : prev.client_secret, + refresh_token: current.settingsName === 'refresh_token' ? current.settingsValue : prev.refresh_token + }; + }, + { + client_id: '', + client_secret: '', + refresh_token: '' + } + ); + urlParams.append('grant_type', 'refresh_token'); + urlParams.append('refresh_token', refresh_token); + urlParams.append('client_id', client_id); + urlParams.append('client_secret', client_secret); + + try { + const tokens$ = this._httpService + .post(`${HUBSTAFF_AUTHORIZATION_URL}/access_tokens`, urlParams, { + headers + }) + .pipe(map((response: AxiosResponse) => response.data)); + const tokens = await lastValueFrom(tokens$); + const settingsDto = settings.map((setting) => { + if (setting.settingsName === 'access_token') { + setting.settingsValue = tokens.access_token; + } + + if (setting.settingsName === 'refresh_token') { + setting.settingsValue = tokens.refresh_token; + } + + return setting; + }) as DeepPartial; + + await this._integrationSettingService.create(settingsDto); + return tokens; + } catch (error) { + throw new BadRequestException(error); + } + } + + /** + * Retrieve the Hubstaff access token for a given integration. + * + * @param integrationId The ID of the integration. + * @returns The integration setting containing the access token. + * @throws NotFoundException if the access token is not found. + */ + async getHubstaffToken(integrationId: ID): Promise { + try { + return await this._integrationSettingService.findOneByWhereOptions({ + integration: { id: integrationId }, + integrationId, + settingsName: 'access_token' + }); + } catch (error) { + throw new NotFoundException(`Access token for integration ID ${integrationId} not found`); + } + } + + /** + * Adds a new Hubstaff integration. + * + * @param body The input data for creating a Hubstaff integration. + * @returns The created or updated integration tenant. + */ + async addIntegration(body: ICreateHubstaffIntegrationInput): Promise { + const tenantId = RequestContext.currentTenantId(); + const { client_id, client_secret, code, redirect_uri, organizationId } = body; + + // Prepare URL search parameters for the Hubstaff token request. + const urlParams = new URLSearchParams(); + urlParams.append('client_id', client_id); + urlParams.append('code', code); + urlParams.append('grant_type', 'authorization_code'); + urlParams.append('redirect_uri', redirect_uri); + urlParams.append('client_secret', client_secret); + + // Find the integration by provider. + const integration = await this._integrationService.findOneByOptions({ + where: { provider: IntegrationEnum.HUBSTAFF } + }); + + // Map project-tied entities with organization and tenant IDs. + const tiedEntities = PROJECT_TIED_ENTITIES.map((entity) => ({ + ...entity, + organizationId, + tenantId + })); + + const entitySettings = DEFAULT_ENTITY_SETTINGS.map((settingEntity) => { + if (settingEntity.entity === IntegrationEntity.PROJECT) { + return { + ...settingEntity, + tiedEntities + }; + } + return { + ...settingEntity, + organizationId, + tenantId + }; + }) as IIntegrationEntitySetting[]; + + 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 IntegrationTenantUpdateOrCreateCommand( + { + name: IntegrationEnum.HUBSTAFF, + integration: { provider: IntegrationEnum.HUBSTAFF }, + tenantId, + organizationId + }, + { + name: IntegrationEnum.HUBSTAFF, + integration, + organizationId, + tenantId, + entitySettings: entitySettings, + settings: [ + { + settingsName: 'client_id', + settingsValue: client_id + }, + { + settingsName: 'client_secret', + settingsValue: client_secret + }, + { + settingsName: 'access_token', + settingsValue: data.access_token + }, + { + settingsName: 'refresh_token', + settingsValue: data.refresh_token + } + ].map((setting) => ({ + ...setting, + tenantId, + organizationId + })) + } + ) + ) + ), + catchError((err) => { + throw new BadRequestException(err); + }) + ); + + return await lastValueFrom(tokens$); + } + + /** + * Fetches and returns a list of organizations from Hubstaff. + * + * @param {string} token - The access token for authentication with the Hubstaff API. + * @returns {Promise} - A promise that resolves to an array of Hubstaff organizations. + * @throws {Error} - Throws an error if the fetch operation fails. + */ + async fetchOrganizations(token: string): Promise { + try { + const response = await this.fetchIntegration('organizations', token); + const { organizations } = response; + return organizations; + } catch (error) { + console.error('Failed to fetch Hubstaff organizations:', error); + throw new Error('Unable to fetch organizations from Hubstaff'); + } + } + + /** + * Fetches and returns a list of projects for a specified organization from Hubstaff. + * + * @param {object} params - The parameters object. + * @param {string} params.organizationId - The ID of the organization. + * @param {string} params.token - The access token for authentication with the Hubstaff API. + * @returns {Promise} - A promise that resolves to an array of Hubstaff projects. + * @throws {Error} - Throws an error if the fetch operation fails. + */ + async fetchOrganizationProjects({ + organizationId, + token + }: { + organizationId: string; + token: string; + }): Promise { + try { + const response = await this.fetchIntegration( + `organizations/${organizationId}/projects?status=all&include=clients`, + token + ); + const { projects } = response; + return projects; + } catch (error) { + console.error('Failed to fetch Hubstaff projects:', error); + throw new Error('Unable to fetch projects from Hubstaff'); + } + } + + /** + * Syncs projects from a third-party integration with the local system. + * + * @param {object} params - The parameters object. + * @param {string} params.integrationId - The ID of the integration. + * @param {string} params.organizationId - The ID of the organization. + * @param {Array<{ sourceId: string }>} params.projects - The list of projects to sync, each containing a sourceId. + * @param {string} params.token - The access token for authentication with the Hubstaff API. + * @returns {Promise} - A promise that resolves to an array of integration maps. + * @throws {HttpException} - Throws an HTTP exception if the sync operation fails. + * @returns + */ + async syncProjects({ + integrationId, + organizationId, + projects, + token + }: { + integrationId: string; + organizationId: string; + projects: Array<{ sourceId: string }>; + token: string; + }): Promise { + try { + const tenantId = RequestContext.currentTenantId(); + return await Promise.all( + projects.map(async ({ sourceId }) => { + const { project } = await this.fetchIntegration( + `projects/${sourceId}`, + token + ); + + /** Third Party Organization Project Map */ + return await this._commandBus.execute( + new IntegrationMapSyncProjectCommand({ + entity: { + name: project.name, + description: project.description, + billable: project.billable, + public: true, + billing: ProjectBillingEnum.RATE, + currency: env.defaultCurrency as CurrenciesEnum, + organizationId, + tenantId, + /** Set Project Budget Here */ + ...(project.budget + ? { + budgetType: project.budget.type || OrganizationProjectBudgetTypeEnum.COST, + startDate: project.budget.start_date || null, + budget: project.budget[ + project.budget.type || OrganizationProjectBudgetTypeEnum.COST + ] + } + : {}) + }, + sourceId, + integrationId, + organizationId, + tenantId + }) + ); + }) + ); + } catch (error) { + console.log( + `Error while syncing ${IntegrationEntity.PROJECT} entity for organization (${organizationId}): %s`, + error?.message + ); + throw new HttpException({ message: error?.message, error }, HttpStatus.BAD_REQUEST); + } + } + + /** + * Syncs organizations from a third-party integration with the local system. + * + * @param {object} params - The parameters object. + * @param {string} params.integrationId - The ID of the integration. + * @param {string} params.organizationId - The ID of the local organization. + * @param {Array<{ sourceId: string }>} params.organizations - The list of organizations to sync, each containing a sourceId. + * @param {string} params.token - The access token for authentication with the Hubstaff API. + * @returns {Promise} - A promise that resolves to an array of integration maps. + * @throws {HttpException} - Throws an HTTP exception if the sync operation fails. + */ + async syncOrganizations({ + integrationId, + organizationId, + organizations, + token + }: { + integrationId: string; + organizationId: string; + organizations: Array<{ sourceId: string }>; + token: string; + }): Promise { + try { + const tenantId = RequestContext.currentTenantId(); + return await Promise.all( + organizations.map(async ({ sourceId }) => { + const { organization } = await this.fetchIntegration( + `organizations/${sourceId}`, + token + ); + /** Third Party Organization Map */ + return await this._commandBus.execute( + new IntegrationMapSyncOrganizationCommand({ + entity: { + name: organization.name, + isActive: organization.status === 'active', + currency: env.defaultCurrency as CurrenciesEnum + }, + sourceId, + integrationId, + organizationId, + tenantId + }) + ); + }) + ); + } catch (error) { + console.log( + `Error while syncing ${IntegrationEntity.ORGANIZATION} entity (${organizationId}): %s`, + error?.message + ); + throw new HttpException({ message: error?.message, error }, HttpStatus.BAD_REQUEST); + } + } + + /** + * Syncs clients from a third-party integration with the local system. + * + * @param {object} params - The parameters object. + * @param {string} params.integrationId - The ID of the integration. + * @param {string} params.organizationId - The ID of the local organization. + * @param {Array<{ id: string, name: string, emails: string[], phone: string, budget?: any }>} params.clients - The list of clients to sync. + * @returns {Promise} - A promise that resolves to an array of integration maps. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncClients({ + integrationId, + organizationId, + clients + }: { + integrationId: string; + organizationId: string; + clients: Array<{ id: string; name: string; emails: string[]; phone: string; budget?: any }>; + }): Promise { + try { + return await Promise.all( + clients.map(async ({ id, name, emails, phone, budget = {} as any }) => { + const { record } = await this._integrationMapService.findOneOrFailByOptions({ + where: { + sourceId: id, + entity: IntegrationEntity.CLIENT, + organizationId + } + }); + if (record) { + return record; + } + + /** + * Set Client Budget Here + */ + let clientBudget = {}; + if (isNotEmpty(budget)) { + clientBudget['budgetType'] = budget.type || OrganizationContactBudgetTypeEnum.COST; + clientBudget['budget'] = budget[clientBudget['budgetType']]; + } + + const gauzyClient = await this._commandBus.execute( + new OrganizationContactCreateCommand({ + name, + organizationId, + primaryEmail: emails[0], + primaryPhone: phone, + contactType: ContactType.CLIENT, + ...clientBudget + }) + ); + return await this._commandBus.execute( + new IntegrationMapSyncEntityCommand({ + gauzyId: gauzyClient.id, + integrationId, + sourceId: id, + entity: IntegrationEntity.CLIENT, + organizationId + }) + ); + }) + ); + } catch (error) { + throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.CLIENT}`); + } + } + + /** + * Syncs screenshots from a third-party integration using timeslot with the local system. + * + * @param {object} params - The parameters object. + * @param {string} params.integrationId - The ID of the integration. + * @param {Array} params.screenshots - The list of screenshots to sync. + * @param {string} params.token - The access token for authentication with the Hubstaff API. + * @param {string} params.organizationId - The ID of the local organization. + * @returns {Promise} - A promise that resolves to an array of integration maps. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncScreenshots({ + integrationId, + screenshots, + token, + organizationId + }: { + integrationId: string; + screenshots: Array; + token: string; + organizationId: string; + }): Promise { + try { + let integratedScreenshots: IIntegrationMap[] = []; + for await (const screenshot of screenshots) { + const { id, user_id } = screenshot; + const employee = await this._getEmployeeByHubstaffUserId(user_id, token, integrationId, organizationId); + integratedScreenshots.push( + await this._commandBus.execute( + new IntegrationMapSyncScreenshotCommand({ + entity: { + employeeId: employee ? employee.gauzyId : null, + ...screenshot + }, + sourceId: id, + integrationId, + organizationId + }) + ) + ); + } + return integratedScreenshots; + } catch (error) { + console.error(`Error syncing screenshots:`, error.message); + throw new BadRequestException(`Can't sync ${IntegrationEntity.SCREENSHOT}`, error.message); + } + } + + /** + * Syncs tasks from a third-party integration with the local system. + * + * @param {object} params - The parameters object. + * @param {string} params.integrationId - The ID of the integration. + * @param {string} params.projectId - The ID of the project to which the tasks belong. + * @param {Array} params.tasks - The list of tasks to sync. + * @param {string} params.organizationId - The ID of the local organization. + * @returns {Promise} - A promise that resolves to an array of integration maps. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncTasks({ + integrationId, + projectId, + tasks, + organizationId + }: { + integrationId: string; + projectId: string; + tasks: Array; + organizationId: string; + }): Promise { + try { + const tenantId = RequestContext.currentTenantId(); + const creatorId = RequestContext.currentUserId(); + + return await Promise.all( + tasks.map(async ({ summary: title, details = null, id, status, due_at }) => { + if (!due_at) { + due_at = new Date(moment().add(2, 'week').format('YYYY-MM-DD HH:mm:ss')); + } + + // Step 1: Execute a command to initiate the synchronization process + const triggeredEvent = false; + return await this._commandBus.execute( + new IntegrationMapSyncTaskCommand( + { + entity: { + title, + projectId, + description: details, + status: status.charAt(0).toUpperCase() + status.slice(1), + creatorId, + dueDate: due_at, + organizationId, + tenantId + }, + sourceId: id, + integrationId, + organizationId, + tenantId + }, + triggeredEvent + ) + ); + }) + ); + } catch (error) { + throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.TASK}`); + } + } + + /** + * Retrieves an employee entity by their user ID from a third-party integration. + * + * @param {string} user_id - The ID of the employee in the third-party integration (Hubstaff). + * @param {string} token - The access token for authentication with the Hubstaff API. + * @param {string} integrationId - The ID of the integration. + * @param {string} organizationId - The ID of the local organization. + * @returns {Promise} - A promise that resolves to the found employee entity. + */ + private async _getEmployeeByHubstaffUserId( + user_id: string, + token: string, + integrationId: string, + organizationId: string + ) { + try { + const tenantId = RequestContext.currentTenantId(); + return await this._integrationMapService.findOneByOptions({ + where: { + sourceId: user_id, + entity: IntegrationEntity.EMPLOYEE, + organizationId, + tenantId + } + }); + } catch (error) { + // If employee is not found in local database, handle the scenario + return await this._handleEmployee({ + user_id, + token, + integrationId, + organizationId + }); + } + } + + /** + * Syncs time slot activities from Hubstaff with the local system. + * + * @param {string} integrationId - The ID of the integration. + * @param {string} organizationId - The ID of the local organization. + * @param {IIntegrationMap} employee - The mapped employee entity from Hubstaff to the local system. + * @param {IHubstaffTimeSlotActivity[]} timeSlots - The list of time slot activities to sync. + * @returns {Promise} - A promise that resolves to an array of mapped time slot activities. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncTimeSlots( + integrationId: string, + organizationId: string, + employee: IIntegrationMap, + timeSlots: IHubstaffTimeSlotActivity[] + ): Promise { + try { + return timeSlots + .filter(async (timeslot: IHubstaffTimeSlotActivity) => { + return !!(await this._commandBus.execute( + new IntegrationMapSyncTimeSlotCommand({ + entity: { + ...timeslot, + employeeId: employee.gauzyId + }, + sourceId: timeslot.id.toString(), + integrationId, + organizationId + }) + )); + }) + .map(({ keyboard, mouse, overall, tracked, time_slot }) => ({ + keyboard, + mouse, + overall, + duration: tracked, + startedAt: time_slot + })); + } catch (error) { + throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.TIME_SLOT}`); + } + } + + /** + * Syncs time logs from Hubstaff with the local system. + * + * @param {any[]} timeLogs - The list of time logs to sync. + * @param {string} token - The access token for authentication with Hubstaff API. + * @param {string} integrationId - The ID of the integration. + * @param {string} organizationId - The ID of the local organization. + * @param {string} projectId - The ID of the project related to the time logs. + * @returns {Promise} - A promise that resolves to an array of integrated time logs. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncTimeLogs( + timeLogs: any, + token: string, + integrationId: string, + organizationId: string, + projectId: string + ): Promise { + try { + let integratedTimeLogs: IIntegrationMap[] = []; + const tenantId = RequestContext.currentTenantId(); + + for await (const timeLog of timeLogs) { + const { id, user_id, task_id, logType, startedAt, stoppedAt, timeSlots } = timeLog; + const employee = await this._getEmployeeByHubstaffUserId(user_id, token, integrationId, organizationId); + const { record } = await this._integrationMapService.findOneOrFailByOptions({ + where: { + sourceId: task_id, + entity: IntegrationEntity.TASK, + organizationId, + tenantId + } + }); + const syncTimeSlots = await this.syncTimeSlots(integrationId, organizationId, employee, timeSlots); + integratedTimeLogs.push( + await this._commandBus.execute( + new IntegrationMapSyncTimeLogCommand({ + entity: { + projectId, + employeeId: employee.gauzyId, + taskId: record ? record.gauzyId : null, + logType, + startedAt, + stoppedAt, + source: TimeLogSourceEnum.HUBSTAFF, + organizationId, + tenantId, + timeSlots: syncTimeSlots + }, + sourceId: id, + integrationId, + organizationId + }) + ) + ); + } + return integratedTimeLogs; + } catch (error) { + throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.TIME_LOG}`); + } + } + + /** + * Syncs an employee from a third-party integration with the local system. + * + * @param {object} params - The parameters object. + * @param {string} params.integrationId - The ID of the integration. + * @param {object} params.user - The user object representing the employee from the third-party integration. + * @param {string} params.organizationId - The ID of the local organization. + * @returns {Promise} - A promise that resolves to the synchronized employee entity. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncEmployee({ + integrationId, + user, + organizationId + }: { + integrationId: string; + user: any; // Define the type of `user` object based on your schema + organizationId: string; + }): Promise { + try { + const tenantId = RequestContext.currentTenantId(); + const { record } = await this._userService.findOneOrFailByOptions({ + where: { + email: user.email, + tenantId + } + }); + let employee; + if (record) { + employee = await this._commandBus.execute(new EmployeeGetCommand({ where: { userId: record.id } })); + } else { + const [role, organization] = await Promise.all([ + await this._roleService.findOneByOptions({ + where: { + name: RolesEnum.EMPLOYEE, + tenantId + } + }), + await this._organizationService.findOneByOptions({ + where: { + id: organizationId, + tenantId + } + }) + ]); + const [firstName, lastName] = user.name.split(' '); + const isActive = user.status === 'active' ? true : false; + employee = await this._commandBus.execute( + new EmployeeCreateCommand({ + user: { + email: user.email, + firstName, + lastName, + role, + tags: null, + tenantId, + preferredComponentLayout: ComponentLayoutStyleEnum.TABLE, + thirdPartyId: user.id + }, + password: env.defaultIntegratedUserPass, + organization, + startedWorkOn: new Date(moment().format('YYYY-MM-DD HH:mm:ss')), + isActive, + tenantId + }) + ); + } + return await this._commandBus.execute( + new IntegrationMapSyncEntityCommand({ + gauzyId: employee.id, + integrationId, + sourceId: user.id, + entity: IntegrationEntity.EMPLOYEE, + organizationId + }) + ); + } catch (error) { + throw new BadRequestException(error, `Can\'t sync ${IntegrationEntity.EMPLOYEE}`); + } + } + + /** + * Handles synchronization of an employee from a third-party integration with the local system. + * + * @param {object} params - The parameters object. + * @param {string} params.user_id - The ID of the user in the third-party integration. + * @param {string} params.integrationId - The ID of the integration. + * @param {string} params.token - The access token for authentication with the third-party API. + * @param {string} params.organizationId - The ID of the local organization. + * @returns {Promise} - A promise that resolves to the synchronized employee entity. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + private async _handleEmployee({ + user_id, + integrationId, + token, + organizationId + }: { + user_id: string; + integrationId: string; + token: string; + organizationId: string; + }): Promise { + try { + const { user } = await this.fetchIntegration(`users/${user_id}`, token); + return await this.syncEmployee({ + integrationId, + user, + organizationId + }); + } catch (error) { + throw new BadRequestException(error, `Can\'t handle ${IntegrationEntity.EMPLOYEE}`); + } + } + + /** + * Handles synchronization of projects from a third-party integration with the local system. + * + * @param {string} sourceId - The ID of the organization in the third-party integration. + * @param {string} integrationId - The ID of the integration. + * @param {string} gauzyId - The ID of the local organization in Gauzy. + * @param {string} token - The access token for authentication with the third-party API. + * @returns {Promise} - A promise that resolves to the synchronized projects. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + private async _handleProjects( + sourceId: string, + integrationId: string, + gauzyId: string, + token: string + ): Promise { + try { + const { projects } = await this.fetchIntegration(`organizations/${sourceId}/projects?status=all`, token); + const projectMap = projects.map(({ name, id, billable, description }) => ({ + name, + sourceId: id, + billable, + description + })); + return await this.syncProjects({ + integrationId, + organizationId: gauzyId, + projects: projectMap, + token + }); + } catch (error) { + throw new BadRequestException(`Can\'t handle ${IntegrationEntity.PROJECT}`); + } + } + + /** + * Handles synchronization of clients from a third-party integration with the local system. + * + * @param {string} sourceId - The ID of the organization in the third-party integration. + * @param {string} integrationId - The ID of the integration. + * @param {string} gauzyId - The ID of the local organization in Gauzy. + * @param {string} token - The access token for authentication with the third-party API. + * @returns {Promise} - A promise that resolves to the synchronized clients. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + private async _handleClients( + sourceId: string, + integrationId: string, + gauzyId: string, + token: string + ): Promise { + try { + const { clients } = await this.fetchIntegration(`organizations/${sourceId}/clients?status=active`, token); + return await this.syncClients({ + integrationId, + organizationId: gauzyId, + clients + }); + } catch (error) { + throw new BadRequestException(error, `Can\'t handle ${IntegrationEntity.CLIENT}`); + } + } + + /** + * Handles synchronization of tasks from a third-party integration with the local system. + * + * @param {any[]} projectsMap - Array of projects mapped with sourceId and gauzyId. + * @param {string} integrationId - The ID of the integration. + * @param {string} token - The access token for authentication with the third-party API. + * @param {string} gauzyId - The ID of the local organization in Gauzy. + * @returns {Promise} - A promise that resolves to an array of synchronized tasks. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + private async _handleTasks( + projectsMap: any[], + integrationId: string, + token: string, + gauzyId: string + ): Promise { + try { + const tasksMap = await Promise.all( + projectsMap.map(async (project) => { + const { tasks } = await this.fetchIntegration(`projects/${project.sourceId}/tasks`, token); + return await this.syncTasks({ + integrationId, + tasks, + projectId: project.gauzyId, + organizationId: gauzyId + }); + }) + ); + return tasksMap; + } catch (error) { + throw new BadRequestException(error, `Can\'t handle ${IntegrationEntity.TASK}`); + } + } + + /** + * Sync URL activities from a third-party integration with the local system. + * + * @param {object} param0 - Parameters for synchronization. + * @param {string} param0.integrationId - The ID of the integration. + * @param {string} param0.projectId - The ID of the project associated with the activities. + * @param {object[]} param0.activities - Array of URL activities to sync. + * @param {string} param0.token - Access token for authentication with the third-party API. + * @param {string} param0.organizationId - The ID of the local organization in Gauzy. + * @returns {Promise} - A promise that resolves to an array of synchronized integration maps. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncUrlActivities({ + integrationId, + projectId, + activities, + token, + organizationId + }): Promise { + try { + const tenantId = RequestContext.currentTenantId(); + return await Promise.all( + await activities.map(async ({ id, site, tracked, user_id, time_slot, task_id }) => { + const time = moment(time_slot).format('HH:mm:ss'); + const date = moment(time_slot).format('YYYY-MM-DD'); + + const employee = await this._getEmployeeByHubstaffUserId( + user_id, + token, + integrationId, + organizationId + ); + const { record: task } = await this._integrationMapService.findOneOrFailByOptions({ + where: { + sourceId: task_id, + entity: IntegrationEntity.TASK, + organizationId, + tenantId + } + }); + const entity: IActivity = { + title: site, + duration: tracked, + type: ActivityType.URL, + time, + date, + projectId, + employeeId: employee ? employee.gauzyId : null, + taskId: task ? task.gauzyId : null, + organizationId, + activityTimestamp: time_slot + }; + return await this._commandBus.execute( + new IntegrationMapSyncActivityCommand({ + entity, + sourceId: id, + integrationId, + organizationId + }) + ); + }) + ); + } catch (error) { + throw new BadRequestException(error, `Can\'t sync URL ${IntegrationEntity.ACTIVITY}`); + } + } + + /** + * Auto-sync URL activities for separate projects within a specified date range. + * + * @param {IIntegrationMap[]} projectsMap - Array of projects to sync URL activities for. + * @param {string} integrationId - The ID of the integration. + * @param {string} token - Access token for authentication with the third-party API. + * @param {string} organizationId - The ID of the local organization in Gauzy. + * @param {IDateRangeActivityFilter} dateRange - Date range filter for activities. + * @returns {Promise} - A promise that resolves to an array of mapped URL activities. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + private async _handleUrlActivities( + projectsMap: IIntegrationMap[], + integrationId: string, + token: string, + organizationId: string, + dateRange: IDateRangeActivityFilter + ) { + try { + const start = moment(dateRange.start).format('YYYY-MM-DD'); + const end = moment(dateRange.end).format('YYYY-MM-DD'); + const pageLimit = 500; + + const urlActivitiesMapped = await Promise.all( + projectsMap.map(async (project) => { + const { gauzyId, sourceId } = project; + const syncedActivities = { + urlActivities: [] + }; + + let stillRecordsAvailable = true; + let nextPageStartId = null; + + while (stillRecordsAvailable) { + let url = `projects/${sourceId}/url_activities?page_limit=${pageLimit}&time_slot[start]=${start}&time_slot[stop]=${end}`; + if (nextPageStartId) { + url += `&page_start_id=${nextPageStartId}`; + } + + const { urls, pagination = {} } = await this.fetchIntegration(url, token); + + if (pagination && pagination.hasOwnProperty('next_page_start_id')) { + const { next_page_start_id } = pagination; + nextPageStartId = next_page_start_id; + stillRecordsAvailable = true; + } else { + nextPageStartId = null; + stillRecordsAvailable = false; + } + syncedActivities.urlActivities.push(urls); + } + + const activities = [].concat.apply([], syncedActivities.urlActivities); + return await this.syncUrlActivities({ + integrationId, + projectId: gauzyId, + activities, + token, + organizationId + }); + }) + ); + return urlActivitiesMapped; + } catch (error) { + throw new BadRequestException(error, `Can\'t handle URL ${IntegrationEntity.ACTIVITY}`); + } + } + + /** + * Sync application activities with the local database. + * + * @param {Object} param0 - Parameters for synchronizing application activities. + * @param {string} param0.integrationId - The ID of the integration. + * @param {string} param0.projectId - The ID of the project associated with the activities. + * @param {any[]} param0.activities - Array of application activities to sync. + * @param {string} param0.token - Access token for authentication with the third-party API. + * @param {string} param0.organizationId - The ID of the local organization in Gauzy. + * @returns {Promise} - A promise that resolves to an array of integration mappings. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + async syncAppActivities({ + integrationId, + projectId, + activities, + token, + organizationId + }): Promise { + try { + const tenantId = RequestContext.currentTenantId(); + + // Map application activities and synchronize them + return await Promise.all( + await activities.map(async ({ id, name, tracked, user_id, time_slot, task_id }) => { + const time = moment(time_slot).format('HH:mm:ss'); + const date = moment(time_slot).format('YYYY-MM-DD'); + + // Fetch employee from local database or sync if not found + const employee = await this._getEmployeeByHubstaffUserId( + user_id, + token, + integrationId, + organizationId + ); + + // Fetch task mapping from local database + const { record: task } = await this._integrationMapService.findOneOrFailByOptions({ + where: { + sourceId: task_id, + entity: IntegrationEntity.TASK, + organizationId, + tenantId + } + }); + + // Prepare activity entity to sync + const entity: IActivity = { + title: name, + duration: tracked, + type: ActivityType.APP, + time, + date, + projectId, + employeeId: employee ? employee.gauzyId : null, + taskId: task ? task.gauzyId : null, + organizationId, + activityTimestamp: time_slot + }; + + // Execute command to sync activity with local database + return await this._commandBus.execute( + new IntegrationMapSyncActivityCommand({ + entity, + sourceId: id, + integrationId, + organizationId + }) + ); + }) + ); + } catch (error) { + throw new BadRequestException(error, `Can\'t sync APP ${IntegrationEntity.ACTIVITY}`); + } + } + + /** + * Auto sync for application activities for separate projects. + * + * @param {IIntegrationMap[]} projectsMap - Array of projects to sync application activities for. + * @param {string} integrationId - The ID of the integration. + * @param {string} token - Access token for authentication with the third-party API. + * @param {string} organizationId - The ID of the local organization in Gauzy. + * @param {IDateRangeActivityFilter} dateRange - Date range filter for activities to sync. + * @returns {Promise} - A promise that resolves to an array of mapped application activities. + * @throws {BadRequestException} - Throws a bad request exception if the sync operation fails. + */ + private async _handleAppActivities( + projectsMap: IIntegrationMap[], + integrationId: string, + token: string, + organizationId: string, + dateRange: IDateRangeActivityFilter + ): Promise { + try { + const start = moment(dateRange.start).format('YYYY-MM-DD'); + const end = moment(dateRange.end).format('YYYY-MM-DD'); + const pageLimit = 500; + + // Map application activities for each project and sync + const appActivitiesMapped = await Promise.all( + projectsMap.map(async (project) => { + const { gauzyId, sourceId } = project; + const syncedActivities = { + applicationActivities: [] + }; + + let stillRecordsAvailable = true; + let nextPageStartId = null; + + // Fetch application activities in paginated manner + while (stillRecordsAvailable) { + let url = `projects/${sourceId}/application_activities?page_limit=${pageLimit}&time_slot[start]=${start}&time_slot[stop]=${end}`; + if (nextPageStartId) { + url += `&page_start_id=${nextPageStartId}`; + } + + const { applications, pagination = {} } = await this.fetchIntegration(url, token); + + // Check for pagination + if (pagination && pagination.hasOwnProperty('next_page_start_id')) { + const { next_page_start_id } = pagination; + nextPageStartId = next_page_start_id; + stillRecordsAvailable = true; + } else { + nextPageStartId = null; + stillRecordsAvailable = false; + } + + // Accumulate fetched activities + syncedActivities.applicationActivities.push(applications); + } + + // Flatten activities array + const activities = [].concat.apply([], syncedActivities.applicationActivities); + + // Sync activities with local database + return await this.syncAppActivities({ + integrationId, + projectId: gauzyId, + activities, + token, + organizationId + }); + }) + ); + + return appActivitiesMapped; + } catch (error) { + console.error(`Error handling APP activities:`, error.message); + throw new BadRequestException(`Can't handle APP ${IntegrationEntity.ACTIVITY}`, error.message); + } + } + + /** + * Auto sync activities (time slot) for separate projects. + * + * @param {IIntegrationMap[]} projectsMap - Array of projects to sync activities for. + * @param {string} integrationId - The ID of the integration. + * @param {string} token - Access token for authentication with the third-party API. + * @param {string} organizationId - The ID of the local organization in Gauzy. + * @param {IDateRangeActivityFilter} dateRange - Date range filter for activities to sync. + * @returns {Promise} - A promise that resolves to an array of integrated time logs. + * @throws {HttpException|BadRequestException} - Throws an HTTP exception or bad request exception if the sync operation fails. + */ + private async _handleActivities( + projectsMap: IIntegrationMap[], + integrationId: string, + token: string, + organizationId: string, + dateRange: IDateRangeActivityFilter + ): Promise { + try { + const start = moment(dateRange.start).format('YYYY-MM-DD'); + const end = moment(dateRange.end).format('YYYY-MM-DD'); + + const integratedTimeLogs: IIntegrationMap[] = []; + + // Iterate over each project and fetch activities + for await (const project of projectsMap) { + const { activities } = await this.fetchIntegration( + `projects/${project.sourceId}/activities?time_slot[start]=${start}&time_slot[stop]=${end}`, + token + ); + + // Skip processing if activities array is empty + if (isEmpty(activities)) { + continue; + } + + // Format fetched activities into time logs + const timeLogs = this.formatLogsFromSlots(activities); + + // Sync formatted time logs with local database + const syncedTimeLogs = await this.syncTimeLogs( + timeLogs, + token, + integrationId, + organizationId, + project.gauzyId + ); + + // Collect integrated time logs + integratedTimeLogs.push(...syncedTimeLogs); + } + return integratedTimeLogs; + } catch (error) { + if (error instanceof HttpException) { + // Re-throw HTTP exceptions with original response and status + throw new HttpException(error.getResponse(), error.getStatus()); + } + // Throw a BadRequestException with detailed error message + throw new BadRequestException(`Can't handle ${IntegrationEntity.ACTIVITY}`, error.message); + } + } + + /** + * Auto sync screenshots activities for separate projects. + * + * @param {IIntegrationMap[]} projectsMap - Array of projects to sync screenshots activities for. + * @param {string} integrationId - The ID of the integration. + * @param {string} token - Access token for authentication with the third-party API. + * @param {string} organizationId - The ID of the local organization in Gauzy. + * @param {IDateRangeActivityFilter} dateRange - Date range filter for screenshots activities to sync. + * @returns {Promise} - A promise that resolves to an array of arrays of integrated screenshots activities. + * @throws {BadRequestException} - Throws a bad request exception with a detailed error message if the sync operation fails. + */ + private async _handleScreenshots( + projectsMap: IIntegrationMap[], + integrationId: string, + token: string, + organizationId: string, + dateRange: IDateRangeActivityFilter + ): Promise { + try { + const start = moment(dateRange.start).format('YYYY-MM-DD'); + const end = moment(dateRange.end).format('YYYY-MM-DD'); + const pageLimit = 500; + + return await Promise.all( + projectsMap.map(async (project) => { + const { sourceId } = project; + const syncedActivities = { + screenshots: [] + }; + + let stillRecordsAvailable = true; + let nextPageStartId = null; + + // Fetch screenshots activities in paginated manner + while (stillRecordsAvailable) { + let url = `projects/${sourceId}/screenshots?page_limit=${pageLimit}&time_slot[start]=${start}&time_slot[stop]=${end}`; + if (nextPageStartId) { + url += `&page_start_id=${nextPageStartId}`; + } + + const { screenshots: fetchScreenshots, pagination = {} } = await this.fetchIntegration( + url, + token + ); + + if (pagination && pagination.hasOwnProperty('next_page_start_id')) { + const { next_page_start_id } = pagination; + nextPageStartId = next_page_start_id; + stillRecordsAvailable = true; + } else { + nextPageStartId = null; + stillRecordsAvailable = false; + } + + syncedActivities.screenshots.push(fetchScreenshots); + } + + // Flatten nested array of screenshots into a single array + const screenshots = [].concat.apply([], syncedActivities.screenshots); + + // Sync fetched screenshots with local database + return await this.syncScreenshots({ + integrationId, + screenshots, + token, + organizationId + }); + }) + ); + } catch (error) { + // Throw a BadRequestException with detailed error message + throw new BadRequestException(`Can't handle activities ${IntegrationEntity.SCREENSHOT}`, error.message); + } + } + + /** + * Automatically synchronize data for integrated entities based on entity settings. + * + * @param {Object} params - Parameters object containing integration details and synchronization configurations. + * @param {string} params.integrationId - The ID of the integration. + * @param {string} params.gauzyId - The ID of the local organization in Gauzy. + * @param {string} params.sourceId - The ID of the organization/source in the external system. + * @param {string} params.token - Access token for authentication with the third-party API. + * @param {IDateRangeActivityFilter} params.dateRange - Date range filter for activities to sync. + * @returns {Promise} - A promise that resolves to an array of objects containing synchronized data for each entity. + * @throws {BadRequestException} - Throws a bad request exception with a detailed error message if any synchronization operation fails. + */ + async autoSync({ + integrationId, + gauzyId, + sourceId, + token, + dateRange + }: { + integrationId: string; + gauzyId: string; + sourceId: string; + token: string; + dateRange: IDateRangeActivityFilter; + }): Promise { + console.log(`${IntegrationEnum.HUBSTAFF} integration start for ${integrationId}`); + /** + * GET organization tenant integration entities settings + */ + const { entitySettings } = await this._integrationTenantService.findOneByIdString(integrationId, { + relations: { + entitySettings: { + tiedEntities: true + } + } + }); + + //entities have depended entity. eg to fetch Task we need Project id or Org id, because our Task entity is related to Project, the relation here is same, we need project id to fetch Tasks + const integratedMaps = await Promise.all( + entitySettings.map(async (setting) => { + switch (setting.entity) { + case IntegrationEntity.PROJECT: + let tasks, activities, screenshots; + const projectsMap: IIntegrationMap[] = await this._handleProjects( + sourceId, + integrationId, + gauzyId, + token + ); + + /** + * Tasks Sync + */ + const taskSetting: IIntegrationEntitySetting = setting.tiedEntities.find( + (res) => res.entity === IntegrationEntity.TASK + ); + if (isObject(taskSetting) && taskSetting.sync) { + tasks = await this._handleTasks(projectsMap, integrationId, token, gauzyId); + } + + /** + * Activity Sync + */ + const activitySetting: IIntegrationEntitySetting = setting.tiedEntities.find( + (res) => res.entity === IntegrationEntity.ACTIVITY + ); + if (isObject(activitySetting) && activitySetting.sync) { + activities = await this._handleActivities( + projectsMap, + integrationId, + token, + gauzyId, + dateRange + ); + activities.application = await this._handleAppActivities( + projectsMap, + integrationId, + token, + gauzyId, + dateRange + ); + activities.urls = await this._handleUrlActivities( + projectsMap, + integrationId, + token, + gauzyId, + dateRange + ); + } + + /** + * Activity Screenshot Sync + */ + const screenshotSetting: IIntegrationEntitySetting = setting.tiedEntities.find( + (res) => res.entity === IntegrationEntity.SCREENSHOT + ); + if (isObject(screenshotSetting) && screenshotSetting.sync) { + screenshots = await this._handleScreenshots( + projectsMap, + integrationId, + token, + gauzyId, + dateRange + ); + } + return { tasks, projectsMap, activities, screenshots }; + case IntegrationEntity.CLIENT: + const clients = await this._handleClients(sourceId, integrationId, gauzyId, token); + return { clients }; + } + }) + ); + console.log(`${IntegrationEnum.HUBSTAFF} integration end for ${integrationId}`); + return integratedMaps; + } + + /** + * Format Hubstaff time slot activities into structured time log entries. + * + * @param {IHubstaffTimeSlotActivity[]} slots - Array of Hubstaff time slot activities. + * @returns {any[]} - Array of structured time log entries. + */ + formatLogsFromSlots(slots: IHubstaffTimeSlotActivity[]): any[] { + if (isEmpty(slots)) { + return; + } + + const range = []; + let i = 0; + while (slots[i]) { + const start = moment(slots[i].starts_at); + const end = moment(slots[i].starts_at).add(slots[i].tracked, 'seconds'); + + range.push({ + start: start.toDate(), + end: end.toDate() + }); + i++; + } + + const timeLogs: Array = []; + const dates: IDateRange[] = mergeOverlappingDateRanges(range); + + if (isNotEmpty(dates)) { + dates.forEach(({ start, end }) => { + let i = 0; + const timeSlots = new Array(); + + while (slots[i]) { + const slotTime = moment(slots[i].starts_at); + if (slotTime.isBetween(moment(start), moment(end), null, '[]')) { + timeSlots.push(slots[i]); + } + i++; + } + + const [activity] = this.getLogsActivityFromSlots(timeSlots); + timeLogs.push({ + startedAt: start, + stoppedAt: end, + timeSlots, + ...activity + }); + }); + } + + return timeLogs; + } + + /** + * Extracts logs activity from time slots. + * + * @param {IHubstaffTimeSlotActivity[]} timeSlots - Array of Hubstaff time slot activities. + * @returns {IHubstaffLogFromTimeSlots[]} - Array of structured log activities. + */ + getLogsActivityFromSlots(timeSlots: IHubstaffTimeSlotActivity[]): IHubstaffLogFromTimeSlots[] { + const timeLogs = timeSlots.reduce((prev, current) => { + const prevLog = prev[current.date]; + return { + ...prev, + [current.date]: prevLog + ? { + id: current.id, + date: current.date, + user_id: prevLog.user_id, + project_id: prevLog.project_id || null, + task_id: prevLog.task_id || null, + // this will take the last chunk(slot), maybe we should allow percentage for this, as one time log can have both manual and tracked + logType: current.client === 'windows' ? TimeLogType.TRACKED : TimeLogType.MANUAL + } + : { + id: current.id, + date: current.date, + user_id: current.user_id, + project_id: current.project_id || null, + task_id: current.task_id || null, + logType: current.client === 'windows' ? TimeLogType.TRACKED : TimeLogType.MANUAL + } + }; + }, {}); + return Object.values(timeLogs); + } +} diff --git a/packages/plugins/integration-hubstaff/src/lib/integration-hubstaff.plugin.ts b/packages/plugins/integration-hubstaff/src/lib/integration-hubstaff.plugin.ts new file mode 100644 index 00000000000..278fd78fcfb --- /dev/null +++ b/packages/plugins/integration-hubstaff/src/lib/integration-hubstaff.plugin.ts @@ -0,0 +1,28 @@ +import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin'; +import { HubstaffModule } from './hubstaff.module'; + +@Plugin({ + imports: [HubstaffModule] +}) +export class IntegrationHubstaffPlugin implements IOnPluginBootstrap, IOnPluginDestroy { + // We disable by default additional logging for each event to avoid cluttering the logs + private logEnabled = true; + + /** + * Called when the plugin is being initialized. + */ + onPluginBootstrap(): void | Promise { + if (this.logEnabled) { + console.log(`${IntegrationHubstaffPlugin.name} is being bootstrapped...`); + } + } + + /** + * Called when the plugin is being destroyed. + */ + onPluginDestroy(): void | Promise { + if (this.logEnabled) { + console.log(`${IntegrationHubstaffPlugin.name} is being destroyed...`); + } + } +} diff --git a/packages/plugins/integration-hubstaff/tsconfig.json b/packages/plugins/integration-hubstaff/tsconfig.json index 999d1cb59b2..3f5e1ef6acf 100644 --- a/packages/plugins/integration-hubstaff/tsconfig.json +++ b/packages/plugins/integration-hubstaff/tsconfig.json @@ -1,13 +1,16 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { - "declaration": true, - "sourceMap": true, - "baseUrl": "./src", - "rootDir": "./src", - "outDir": "./dist", - "types": ["node", "jest"] + "module": "commonjs" }, - "include": ["./src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/packages/plugins/integration-hubstaff/tsconfig.lib.json b/packages/plugins/integration-hubstaff/tsconfig.lib.json index ea6be8e9a50..9436f1db06d 100644 --- a/packages/plugins/integration-hubstaff/tsconfig.lib.json +++ b/packages/plugins/integration-hubstaff/tsconfig.lib.json @@ -1,3 +1,14 @@ { - "extends": "./tsconfig.json" + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "sourceMap": true, + "baseUrl": "./src", + "rootDir": "./src", + "outDir": "./dist", + "types": ["node", "jest"], + "target": "es6" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/plugins/integration-hubstaff/tsconfig.lib.prod.json b/packages/plugins/integration-hubstaff/tsconfig.lib.prod.json new file mode 100644 index 00000000000..1696bce1ffd --- /dev/null +++ b/packages/plugins/integration-hubstaff/tsconfig.lib.prod.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declaration": true, + "sourceMap": false, + "removeComments": true, + "noEmitHelpers": true, + "importHelpers": true, + "target": "es6", + "outDir": "./dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/plugins/integration-hubstaff/tsconfig.spec.json b/packages/plugins/integration-hubstaff/tsconfig.spec.json index 9f405553401..64e5ea1a7d4 100644 --- a/packages/plugins/integration-hubstaff/tsconfig.spec.json +++ b/packages/plugins/integration-hubstaff/tsconfig.spec.json @@ -1,15 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", + "outDir": "./dist/spec", "module": "commonjs", "types": ["jest", "node"] }, - "include": [ - "**/*.spec.ts", - "**/*.spec.tsx", - "**/*.spec.js", - "**/*.spec.jsx", - "**/*.d.ts" - ] + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] } diff --git a/packages/plugins/integration-hubstaff/tslint.json b/packages/plugins/integration-hubstaff/tslint.json deleted file mode 100644 index 74516cfb290..00000000000 --- a/packages/plugins/integration-hubstaff/tslint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../../tslint.json", - "rules": [] -} diff --git a/packages/plugins/integration-upwork/README.md b/packages/plugins/integration-upwork/README.md index 989481be380..0c0c8a4d2f1 100644 --- a/packages/plugins/integration-upwork/README.md +++ b/packages/plugins/integration-upwork/README.md @@ -4,8 +4,8 @@ This library was generated with [Nx](https://nx.dev). ## Building -Run `nx build integration-upwork` to build the library. +Run `yarn run build` to build the library. ## Running unit tests -Run `nx test integration-upwork` to execute the unit tests via [Jest](https://jestjs.io). +Run `yarn run test:e2e` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/plugins/integration-upwork/package.json b/packages/plugins/integration-upwork/package.json index a351f6ffbd3..bb64d95509b 100644 --- a/packages/plugins/integration-upwork/package.json +++ b/packages/plugins/integration-upwork/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/integration-upwork", + "name": "@gauzy/plugin-integration-upwork", "version": "0.1.0", "description": "Ever Gauzy Platform plugin for integration with Upwork APIs", "author": { @@ -44,8 +44,8 @@ }, "scripts": { "test:e2e": "jest --config ./jest.config.ts", - "build": "rimraf dist && yarn run compile", - "compile": "tsc -p tsconfig.lib.json" + "build": "rimraf dist && tsc -p tsconfig.lib.json", + "build:prod": "rimraf dist && tsc -p tsconfig.lib.prod.json" }, "peerDependencies": { "tslib": "^2.6.2" @@ -55,31 +55,35 @@ "@gauzy/config": "^0.1.0", "@gauzy/contracts": "^0.1.0", "@gauzy/core": "^0.1.0", - "@gauzy/job-proposal-plugin": "^0.1.0", "@gauzy/plugin": "^0.1.0", - "@mikro-orm/nestjs": "^5.2.3", + "@gauzy/plugin-job-proposal": "^0.1.0", "@nestjs/common": "^10.3.7", "@nestjs/cqrs": "^10.2.7", "@nestjs/platform-express": "^10.3.7", "@nestjs/swagger": "^7.3.0", - "@nestjs/typeorm": "^10.0.2", + "class-validator": "^0.14.0", + "csv-parser": "^2.3.2", "express": "^4.18.2", + "fs-extra": "^10.1.0", "moment": "^2.30.1", + "typeorm": "^0.3.20", "underscore": "^1.13.3", "upwork-api": "^1.3.8", - "uuid": "^8.3.0", - "fs-extra": "^10.1.0", - "csv-parser": "^2.3.2", - "class-validator": "^0.14.0", - "typeorm": "^0.3.20" + "uuid": "^8.3.0" }, "devDependencies": { "@types/express": "^4.17.13", - "@types/node": "^20.14.9", - "@types/multer": "^1.4.11", "@types/fs-extra": "5.0.2", + "@types/jest": "^29.4.4", + "@types/multer": "^1.4.11", + "@types/node": "^20.14.9", "@types/uuid": "^3.4.4", "rimraf": "^3.0.2", "typescript": "5.1.6" - } + }, + "engines": { + "node": ">=20.11.1", + "yarn": ">=1.22.19" + }, + "sideEffects": false } diff --git a/packages/plugins/integration-upwork/project.json b/packages/plugins/integration-upwork/project.json index 0033b62fd54..2455da683a1 100644 --- a/packages/plugins/integration-upwork/project.json +++ b/packages/plugins/integration-upwork/project.json @@ -1,5 +1,5 @@ { - "name": "integration-upwork", + "name": "plugin-integration-upwork", "$schema": "../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "packages/plugins/integration-upwork/src", "projectType": "library", diff --git a/packages/plugins/integration-upwork/src/lib/integration-upwork.plugin.ts b/packages/plugins/integration-upwork/src/lib/integration-upwork.plugin.ts index 61da48964af..d6d1131330a 100644 --- a/packages/plugins/integration-upwork/src/lib/integration-upwork.plugin.ts +++ b/packages/plugins/integration-upwork/src/lib/integration-upwork.plugin.ts @@ -1,10 +1,8 @@ import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin'; -import { ApplicationPluginConfig } from '@gauzy/common'; import { UpworkModule } from './upwork.module'; @Plugin({ - imports: [UpworkModule], - configuration: (config: ApplicationPluginConfig) => config + imports: [UpworkModule] }) export class IntegrationUpworkPlugin implements IOnPluginBootstrap, IOnPluginDestroy { // We disable by default additional logging for each event to avoid cluttering the logs diff --git a/packages/plugins/integration-upwork/src/lib/upwork.module.ts b/packages/plugins/integration-upwork/src/lib/upwork.module.ts index 99286d1bc3b..ece8e76934e 100644 --- a/packages/plugins/integration-upwork/src/lib/upwork.module.ts +++ b/packages/plugins/integration-upwork/src/lib/upwork.module.ts @@ -14,7 +14,7 @@ import { TimeSlotModule, UserModule } from '@gauzy/core'; -import { ProposalModule } from '@gauzy/job-proposal-plugin'; +import { ProposalModule } from '@gauzy/plugin-job-proposal'; import { UpworkTransactionService } from './upwork-transaction.service'; import { UpworkService } from './upwork.service'; import { UpworkJobService } from './upwork-job.service'; diff --git a/packages/plugins/integration-upwork/src/lib/upwork.service.ts b/packages/plugins/integration-upwork/src/lib/upwork.service.ts index 2fbb7bf990c..eb70509c0e6 100644 --- a/packages/plugins/integration-upwork/src/lib/upwork.service.ts +++ b/packages/plugins/integration-upwork/src/lib/upwork.service.ts @@ -70,7 +70,7 @@ import { TimeLogCreateCommand, TimeSlotCreateCommand } from '@gauzy/core'; -import { ProposalCreateCommand } from '@gauzy/job-proposal-plugin'; +import { ProposalCreateCommand } from '@gauzy/plugin-job-proposal'; import { UpworkReportService } from './upwork-report.service'; import { UpworkJobService } from './upwork-job.service'; import { UpworkOffersService } from './upwork-offers.service'; diff --git a/packages/plugins/integration-upwork/tsconfig.lib.prod.json b/packages/plugins/integration-upwork/tsconfig.lib.prod.json new file mode 100644 index 00000000000..1696bce1ffd --- /dev/null +++ b/packages/plugins/integration-upwork/tsconfig.lib.prod.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declaration": true, + "sourceMap": false, + "removeComments": true, + "noEmitHelpers": true, + "importHelpers": true, + "target": "es6", + "outDir": "./dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/plugins/jitsu-analytics/.dockerignore b/packages/plugins/jitsu-analytics/.dockerignore index 3438721757c..6edd0523636 100644 --- a/packages/plugins/jitsu-analytics/.dockerignore +++ b/packages/plugins/jitsu-analytics/.dockerignore @@ -1,10 +1,20 @@ +docker +tmp +README.md +.env + +# git + .git .gitignore .gitmodules -README.md -docker + +# dependencies + node_modules -tmp -build + +# misc + +npm-debug.log dist -.env +build diff --git a/packages/plugins/jitsu-analytics/.eslintrc.json b/packages/plugins/jitsu-analytics/.eslintrc.json new file mode 100644 index 00000000000..79fd7c1d982 --- /dev/null +++ b/packages/plugins/jitsu-analytics/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/plugins/jitsu-analytics/.gitignore b/packages/plugins/jitsu-analytics/.gitignore new file mode 100644 index 00000000000..fe30e94bebf --- /dev/null +++ b/packages/plugins/jitsu-analytics/.gitignore @@ -0,0 +1,6 @@ +# dependencies +node_modules/ + +# misc +npm-debug.log +dist diff --git a/packages/plugins/jitsu-analytics/.npmignore b/packages/plugins/jitsu-analytics/.npmignore new file mode 100644 index 00000000000..1eb4beb9572 --- /dev/null +++ b/packages/plugins/jitsu-analytics/.npmignore @@ -0,0 +1,4 @@ +# .npmignore + +src/ +node_modules/ diff --git a/packages/plugins/jitsu-analytics/README.md b/packages/plugins/jitsu-analytics/README.md index 90bf0487c82..6c0eab7df0d 100644 --- a/packages/plugins/jitsu-analytics/README.md +++ b/packages/plugins/jitsu-analytics/README.md @@ -16,7 +16,7 @@ The Jitsu Analytics Plugin seamlessly integrates your server with Jitsu Analytic Install the Jitsu Analytics Plugin using your preferred package manager: ```bash -npm install @jitsu/analytics-plugin +npm install @gauzy/plugin-jitsu-analytics # or -yarn add @jitsu/analytics-plugin +yarn add @gauzy/plugin-jitsu-analytics ``` diff --git a/packages/plugins/jitsu-analytics/jest.config.js b/packages/plugins/jitsu-analytics/jest.config.js deleted file mode 100644 index cbc35393306..00000000000 --- a/packages/plugins/jitsu-analytics/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - name: 'jitsu-analytics', - preset: '../../../jest.config.js', - transform: { - '^.+\\.[tj]sx?$': 'ts-jest' - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], - coverageDirectory: '../../coverage/plugins/jitsu-analytics' -}; diff --git a/packages/plugins/jitsu-analytics/jest.config.ts b/packages/plugins/jitsu-analytics/jest.config.ts new file mode 100644 index 00000000000..2c5e7cd1d42 --- /dev/null +++ b/packages/plugins/jitsu-analytics/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'jitsu-analytics', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }] + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/packages/plugins/jitsu-analytics' +}; diff --git a/packages/plugins/jitsu-analytics/package.json b/packages/plugins/jitsu-analytics/package.json index 26f56d931b2..e16fc4ae5b7 100644 --- a/packages/plugins/jitsu-analytics/package.json +++ b/packages/plugins/jitsu-analytics/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/jitsu-analytics-plugin", + "name": "@gauzy/plugin-jitsu-analytics", "version": "0.1.0", "description": "Ever Gauzy Platform Jitsu Analytics Plugin", "author": { @@ -8,6 +8,22 @@ "url": "https://ever.co" }, "license": "AGPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/ever-co/ever-gauzy" + }, + "bugs": { + "url": "https://github.com/ever-co/ever-gauzy/issues" + }, + "homepage": "https://ever.co", + "keywords": [ + "gauzy", + "plugin", + "analytics", + "jitsu", + "ever", + "platform" + ], "private": true, "main": "dist/index.js", "types": "dist/index.d.ts", @@ -22,13 +38,16 @@ "access": "restricted" }, "scripts": { - "test:e2e": "jest --config ./jest.config.js", - "build": "rimraf dist && yarn run compile", - "compile": "tsc -p tsconfig.build.json" + "test:e2e": "jest --config ./jest.config.ts", + "build": "rimraf dist && tsc -p tsconfig.lib.json", + "build:prod": "rimraf dist && tsc -p tsconfig.lib.prod.json" + }, + "peerDependencies": { + "tslib": "^2.6.2" }, - "keywords": [], "dependencies": { "@gauzy/config": "^0.1.0", + "@gauzy/core": "^0.1.0", "@gauzy/plugin": "^0.1.0", "@jitsu/js": "^1.8.2", "@nestjs/common": "^10.3.7", @@ -37,8 +56,15 @@ "typeorm": "^0.3.20" }, "devDependencies": { + "@types/jest": "^29.4.4", "@types/node": "^20.14.9", "rimraf": "^3.0.2", - "typescript": "5.1.6" - } + "typescript": "5.1.6", + "tslint": "^6.1.3" + }, + "engines": { + "node": ">=20.11.1", + "yarn": ">=1.22.19" + }, + "sideEffects": false } diff --git a/packages/plugins/jitsu-analytics/project.json b/packages/plugins/jitsu-analytics/project.json new file mode 100644 index 00000000000..2e3b0b17159 --- /dev/null +++ b/packages/plugins/jitsu-analytics/project.json @@ -0,0 +1,48 @@ +{ + "name": "jitsu-analytics", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugins/jitsu-analytics/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "./packages/plugins/jitsu-analytics/dist", + "tsConfig": "packages/plugins/jitsu-analytics/tsconfig.lib.json", + "packageJson": "packages/plugins/jitsu-analytics/package.json", + "main": "packages/plugins/jitsu-analytics/src/index.ts", + "assets": ["packages/plugins/jitsu-analytics/*.md"] + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "node tools/scripts/publish.mjs plugins-jitsu-analytics {args.ver} {args.tag}" + }, + "dependsOn": ["build"] + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/plugins/jitsu-analytics/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/plugins/jitsu-analytics/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + }, + "tags": [] +} diff --git a/packages/plugins/jitsu-analytics/src/index.ts b/packages/plugins/jitsu-analytics/src/index.ts index 3c9e561fc3d..89c64cbc6b1 100644 --- a/packages/plugins/jitsu-analytics/src/index.ts +++ b/packages/plugins/jitsu-analytics/src/index.ts @@ -1 +1 @@ -export * from './jitsu-analytics.plugin'; +export * from './lib/jitsu-analytics.plugin'; diff --git a/packages/plugins/jitsu-analytics/src/jitsu-helper.ts b/packages/plugins/jitsu-analytics/src/jitsu-helper.ts deleted file mode 100644 index 350d7891ac7..00000000000 --- a/packages/plugins/jitsu-analytics/src/jitsu-helper.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { environment } from "@gauzy/config"; -import { AnalyticsInterface, JitsuOptions, jitsuAnalytics } from "@jitsu/js"; -import fetch from 'node-fetch'; -import { JitsuModuleOptions } from "./jitsu.types"; - -/** - * Parses the options for Jitsu Analytics. - * @param options The input options object. - * @returns A record containing parsed Jitsu module options. - */ -export const parseOptions = (options: JitsuModuleOptions): JitsuModuleOptions => ({ - // If the 'isGlobal' property is defined in the input options, use its value; otherwise, default to true. - isGlobal: options.isGlobal ?? true, - - // Parse the configuration using the 'parseConfig' function and assign the result to the 'config' property. - config: parseConfig(options.config), // -}); - -/** - * Parse the configuration for Jitsu Analytics. - * @param config The input configuration object. - * @returns A record containing Jitsu configuration properties. - */ -export const parseConfig = (config: JitsuOptions): Record => ({ - host: config.host || environment.jitsu.serverHost || '', // Use serverHost from environment or empty string as default - writeKey: config.writeKey || environment.jitsu.serverWriteKey || '', // Use serverWriteKey from environment or empty string as default - debug: config.debug || false, // Use debug from input config or false as default - echoEvents: config.echoEvents || false, // Use echoEvents from input config or false as default -}); - -/** - * Create a Jitsu Analytics instance. - * @param opts The JitsuOptions object for configuration. - * @returns An instance of Jitsu Analytics. - */ -export const createJitsu = (opts: JitsuOptions): AnalyticsInterface => { - // Parse the configuration options - const config = parseConfig(opts); - - if (!config.host || !config.writeKey) { - // Handle the case where 'host' or 'writeKey' is missing - console.error('Jitsu Analytics initialization failed: Missing host or writeKey.'); - return; - } - - config.fetch = fetch; // Assign the 'fetch' function to 'fetch' - - // Create and return a Jitsu Analytics instance with the parsed configuration properties - return jitsuAnalytics({ - ...config, // Spread the parsed configuration properties - }); -}; diff --git a/packages/plugins/jitsu-analytics/src/jitsu-analytics.plugin.ts b/packages/plugins/jitsu-analytics/src/lib/jitsu-analytics.plugin.ts similarity index 84% rename from packages/plugins/jitsu-analytics/src/jitsu-analytics.plugin.ts rename to packages/plugins/jitsu-analytics/src/lib/jitsu-analytics.plugin.ts index dba655aa333..81bcdf26fa3 100644 --- a/packages/plugins/jitsu-analytics/src/jitsu-analytics.plugin.ts +++ b/packages/plugins/jitsu-analytics/src/lib/jitsu-analytics.plugin.ts @@ -1,21 +1,19 @@ import { DynamicModule } from '@nestjs/common'; -import { GauzyCorePlugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin'; +import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin'; import { JITSU_MODULE_PROVIDER_CONFIG, JitsuModuleOptions } from './jitsu.types'; import { parseOptions } from './jitsu-helper'; import { JitsuAnalyticsService } from './jitsu-analytics.service'; import { JitsuEventsSubscriber } from './jitsu-events.subscriber'; -@GauzyCorePlugin({ +@Plugin({ providers: [ JitsuAnalyticsService, { provide: JITSU_MODULE_PROVIDER_CONFIG, - useFactory: () => JitsuAnalyticsPlugin.options?.config, - }, + useFactory: () => JitsuAnalyticsPlugin.options?.config + } ], - subscribers: [ - JitsuEventsSubscriber - ] + subscribers: [JitsuEventsSubscriber] }) export class JitsuAnalyticsPlugin implements IOnPluginBootstrap, IOnPluginDestroy { static options: JitsuModuleOptions = {} as any; @@ -57,10 +55,10 @@ export class JitsuAnalyticsPlugin implements IOnPluginBootstrap, IOnPluginDestro JitsuAnalyticsService, { provide: JITSU_MODULE_PROVIDER_CONFIG, - useFactory: () => options.config, - }, + useFactory: () => options.config + } ], - exports: [JitsuAnalyticsService], + exports: [JitsuAnalyticsService] }; } } diff --git a/packages/plugins/jitsu-analytics/src/jitsu-analytics.service.ts b/packages/plugins/jitsu-analytics/src/lib/jitsu-analytics.service.ts similarity index 100% rename from packages/plugins/jitsu-analytics/src/jitsu-analytics.service.ts rename to packages/plugins/jitsu-analytics/src/lib/jitsu-analytics.service.ts diff --git a/packages/plugins/jitsu-analytics/src/jitsu-events.subscriber.ts b/packages/plugins/jitsu-analytics/src/lib/jitsu-events.subscriber.ts similarity index 97% rename from packages/plugins/jitsu-analytics/src/jitsu-events.subscriber.ts rename to packages/plugins/jitsu-analytics/src/lib/jitsu-events.subscriber.ts index 1f448a8da1f..1fa4202f753 100644 --- a/packages/plugins/jitsu-analytics/src/jitsu-events.subscriber.ts +++ b/packages/plugins/jitsu-analytics/src/lib/jitsu-events.subscriber.ts @@ -12,7 +12,6 @@ const { jitsu } = environment; /* Global Entity Subscriber - Listens to all entity inserts updates and removal then sends to Jitsu */ @EventSubscriber() export class JitsuEventsSubscriber extends BaseEntityEventSubscriber { - private readonly logger = new Logger(JitsuEventsSubscriber.name); private readonly jitsuAnalytics: AnalyticsInterface; @@ -81,7 +80,7 @@ export class JitsuEventsSubscriber extends BaseEntityEventSubscriber { this.logger.log(`AFTER ENTITY UPDATED: `, JSON.stringify(entity)); } - // Track the updation event with Jitsu Analytics asynchronously + // Track the update event with Jitsu Analytics asynchronously this.analyticsTrack('afterEntityUpdate', { data: { entity } }); } catch (error) { // Error handling logic @@ -125,7 +124,10 @@ export class JitsuEventsSubscriber extends BaseEntityEventSubscriber { try { if (this.logEnabled) { - this.logger.log(`Before Jitsu Tracking Entity Events: ${event}`, chalk.magenta(JSON.stringify(properties))); + this.logger.log( + `Before Jitsu Tracking Entity Events: ${event}`, + chalk.magenta(JSON.stringify(properties)) + ); } // Track the event diff --git a/packages/plugins/jitsu-analytics/src/lib/jitsu-helper.ts b/packages/plugins/jitsu-analytics/src/lib/jitsu-helper.ts new file mode 100644 index 00000000000..62aa6702b28 --- /dev/null +++ b/packages/plugins/jitsu-analytics/src/lib/jitsu-helper.ts @@ -0,0 +1,52 @@ +import { environment } from '@gauzy/config'; +import { AnalyticsInterface, JitsuOptions, jitsuAnalytics } from '@jitsu/js'; +import fetch from 'node-fetch'; +import { JitsuModuleOptions } from './jitsu.types'; + +/** + * Parses the options for Jitsu Analytics. + * @param options The input options object. + * @returns A record containing parsed Jitsu module options. + */ +export const parseOptions = (options: JitsuModuleOptions): JitsuModuleOptions => ({ + // If the 'isGlobal' property is defined in the input options, use its value; otherwise, default to true. + isGlobal: options.isGlobal ?? true, + + // Parse the configuration using the 'parseConfig' function and assign the result to the 'config' property. + config: parseConfig(options.config) // +}); + +/** + * Parse the configuration for Jitsu Analytics. + * @param config The input configuration object. + * @returns A record containing Jitsu configuration properties. + */ +export const parseConfig = (config: JitsuOptions): Record => ({ + host: config.host || environment.jitsu.serverHost || '', // Use serverHost from environment or empty string as default + writeKey: config.writeKey || environment.jitsu.serverWriteKey || '', // Use serverWriteKey from environment or empty string as default + debug: config.debug || false, // Use debug from input config or false as default + echoEvents: config.echoEvents || false // Use echoEvents from input config or false as default +}); + +/** + * Create a Jitsu Analytics instance. + * @param opts The JitsuOptions object for configuration. + * @returns An instance of Jitsu Analytics. + */ +export const createJitsu = (opts: JitsuOptions): AnalyticsInterface => { + // Parse the configuration options + const config = parseConfig(opts); + + if (!config.host || !config.writeKey) { + // Handle the case where 'host' or 'writeKey' is missing + console.error('Jitsu Analytics initialization failed: Missing host or writeKey.'); + return; + } + + config.fetch = fetch; // Assign the 'fetch' function to 'fetch' + + // Create and return a Jitsu Analytics instance with the parsed configuration properties + return jitsuAnalytics({ + ...config // Spread the parsed configuration properties + }); +}; diff --git a/packages/plugins/jitsu-analytics/src/jitsu.types.ts b/packages/plugins/jitsu-analytics/src/lib/jitsu.types.ts similarity index 100% rename from packages/plugins/jitsu-analytics/src/jitsu.types.ts rename to packages/plugins/jitsu-analytics/src/lib/jitsu.types.ts diff --git a/packages/plugins/jitsu-analytics/tsconfig.build.json b/packages/plugins/jitsu-analytics/tsconfig.build.json deleted file mode 100644 index ea6be8e9a50..00000000000 --- a/packages/plugins/jitsu-analytics/tsconfig.build.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./tsconfig.json" -} diff --git a/packages/plugins/jitsu-analytics/tsconfig.json b/packages/plugins/jitsu-analytics/tsconfig.json index 999d1cb59b2..3f5e1ef6acf 100644 --- a/packages/plugins/jitsu-analytics/tsconfig.json +++ b/packages/plugins/jitsu-analytics/tsconfig.json @@ -1,13 +1,16 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { - "declaration": true, - "sourceMap": true, - "baseUrl": "./src", - "rootDir": "./src", - "outDir": "./dist", - "types": ["node", "jest"] + "module": "commonjs" }, - "include": ["./src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/packages/plugins/jitsu-analytics/tsconfig.lib.json b/packages/plugins/jitsu-analytics/tsconfig.lib.json new file mode 100644 index 00000000000..9436f1db06d --- /dev/null +++ b/packages/plugins/jitsu-analytics/tsconfig.lib.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "sourceMap": true, + "baseUrl": "./src", + "rootDir": "./src", + "outDir": "./dist", + "types": ["node", "jest"], + "target": "es6" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/plugins/jitsu-analytics/tsconfig.lib.prod.json b/packages/plugins/jitsu-analytics/tsconfig.lib.prod.json new file mode 100644 index 00000000000..1696bce1ffd --- /dev/null +++ b/packages/plugins/jitsu-analytics/tsconfig.lib.prod.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declaration": true, + "sourceMap": false, + "removeComments": true, + "noEmitHelpers": true, + "importHelpers": true, + "target": "es6", + "outDir": "./dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/plugins/jitsu-analytics/tsconfig.spec.json b/packages/plugins/jitsu-analytics/tsconfig.spec.json index 9f405553401..930a5bcfbbc 100644 --- a/packages/plugins/jitsu-analytics/tsconfig.spec.json +++ b/packages/plugins/jitsu-analytics/tsconfig.spec.json @@ -1,15 +1,9 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", + "outDir": "../../../dist/out-tsc", "module": "commonjs", "types": ["jest", "node"] }, - "include": [ - "**/*.spec.ts", - "**/*.spec.tsx", - "**/*.spec.js", - "**/*.spec.jsx", - "**/*.d.ts" - ] + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] } diff --git a/packages/plugins/job-proposal/README.md b/packages/plugins/job-proposal/README.md index c9eeb1b4c6d..996f46f3738 100644 --- a/packages/plugins/job-proposal/README.md +++ b/packages/plugins/job-proposal/README.md @@ -9,7 +9,7 @@ To install the Job Proposal Plugin, simply run the following command in your terminal: ```bash -npm install @gauzy/job-proposal-plugin +npm install @gauzy/plugin-job-proposal # or -yarn add @gauzy/job-proposal-plugin +yarn add @gauzy/plugin-job-proposal ``` diff --git a/packages/plugins/job-proposal/package.json b/packages/plugins/job-proposal/package.json index 1c2dbadeca8..9c0e945ac03 100644 --- a/packages/plugins/job-proposal/package.json +++ b/packages/plugins/job-proposal/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/job-proposal-plugin", + "name": "@gauzy/plugin-job-proposal", "version": "0.1.0", "description": "Ever Gauzy Platform Job Proposal Plugin", "author": { diff --git a/packages/plugins/job-proposal/src/job-proposal.plugin.ts b/packages/plugins/job-proposal/src/job-proposal.plugin.ts index d7007f4e98c..036bae23c49 100644 --- a/packages/plugins/job-proposal/src/job-proposal.plugin.ts +++ b/packages/plugins/job-proposal/src/job-proposal.plugin.ts @@ -12,7 +12,7 @@ import { ProposalSeederService } from './proposal/proposal-seeder.service'; entities: [Proposal, EmployeeProposalTemplate], configuration: (config: ApplicationPluginConfig) => { config.customFields.Tag.push({ - propertyPath: 'proposals', + name: 'proposals', type: 'relation', relationType: 'many-to-many', pivotTable: 'tag_proposal', diff --git a/packages/plugins/job-search-ui/README.md b/packages/plugins/job-search-ui/README.md index f646a24dd19..bb95cc48efd 100644 --- a/packages/plugins/job-search-ui/README.md +++ b/packages/plugins/job-search-ui/README.md @@ -1,24 +1,24 @@ -# @gauzy/job-search-ui-plugin +# @gauzy/plugin-job-search-ui This library was generated with [Nx](https://nx.dev). ## Code scaffolding -Run `ng generate component component-name --project job-search-ui-plugin` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project job-search-ui-plugin`. +Run `ng generate component component-name --project plugin-job-search-ui` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project plugin-job-search-ui`. -> Note: Don't forget to add `--project job-search-ui-plugin` or else it will be added to the default project in your `angular.json` file. +> Note: Don't forget to add `--project plugin-job-search-ui` or else it will be added to the default project in your `angular.json` file. ## Build -Run `ng build job-search-ui-plugin` to build the project. The build artifacts will be stored in the `dist/plugins/` directory. +Run `ng build plugin-job-search-ui` to build the project. The build artifacts will be stored in the `dist/plugins/` directory. ## Publishing -After building your library with `ng build job-search-ui-plugin`, go to the dist folder `cd dist/packages/plugins/job-search-ui` and run `npm publish`. +After building your library with `ng build plugin-job-search-ui`, go to the dist folder `cd dist/packages/plugins/job-search-ui` and run `npm publish`. ## Running unit tests -Run `ng test job-search-ui-plugin` to execute the unit tests via [Karma](https://karma-runner.github.io). +Run `ng test plugin-job-search-ui` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Further help diff --git a/packages/plugins/job-search-ui/jest.config.ts b/packages/plugins/job-search-ui/jest.config.ts index ca07c95f4a7..894f3f98f6b 100644 --- a/packages/plugins/job-search-ui/jest.config.ts +++ b/packages/plugins/job-search-ui/jest.config.ts @@ -1,9 +1,9 @@ /* eslint-disable */ export default { - displayName: 'plugins-job-search-ui-plugin', + displayName: 'plugin-job-search-ui', preset: '../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../../../coverage/packages/plugins/job-search-ui-plugin', + coverageDirectory: '../../../coverage/packages/plugins/plugin-job-search-ui', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', diff --git a/packages/plugins/job-search-ui/package.json b/packages/plugins/job-search-ui/package.json index 00a7cb5bcd8..bbb92ce1c17 100644 --- a/packages/plugins/job-search-ui/package.json +++ b/packages/plugins/job-search-ui/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/job-search-ui-plugin", + "name": "@gauzy/plugin-job-search-ui", "version": "0.1.0", "description": "A UI plugin for job search functionality in the Gauzy platform.", "author": { @@ -10,9 +10,9 @@ "license": "AGPL-3.0", "private": true, "scripts": { - "lib:build": "ng build job-search-ui-plugin --configuration=development", - "lib:build:prod": "ng build job-search-ui-plugin --configuration=production", - "lib:watch": "ng build job-search-ui-plugin--watch --configuration=development", + "lib:build": "ng build plugin-job-search-ui --configuration=development", + "lib:build:prod": "ng build plugin-job-search-ui --configuration=production", + "lib:watch": "ng build plugin-job-search-ui--watch --configuration=development", "clean": "rimraf dist" }, "peerDependencies": { diff --git a/packages/plugins/job-search-ui/src/lib/job-search/job-search-ui.component.ts b/packages/plugins/job-search-ui/src/lib/job-search/job-search-ui.component.ts index fe0917b8803..3c1a9386847 100644 --- a/packages/plugins/job-search-ui/src/lib/job-search/job-search-ui.component.ts +++ b/packages/plugins/job-search-ui/src/lib/job-search/job-search-ui.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ - selector: 'job-search-ui-plugin', + selector: 'job-search-ui-component', template: `

job-search-ui plugin works!!!!!!!!!!

`, styles: [] }) diff --git a/packages/plugins/job-search/README.md b/packages/plugins/job-search/README.md index 9824721f417..cdec6d0da22 100644 --- a/packages/plugins/job-search/README.md +++ b/packages/plugins/job-search/README.md @@ -14,7 +14,7 @@ The Job Search Plugin is a powerful tool designed to enhance your job search exp To install the Job Search Plugin, simply run the following command in your terminal: ```bash -npm install @gauzy/job-search-plugin +npm install @gauzy/plugin-job-search # or -yarn add @gauzy/job-search-plugin +yarn add @gauzy/plugin-job-search ``` diff --git a/packages/plugins/job-search/package.json b/packages/plugins/job-search/package.json index 93f8bc09be4..7ac68c02eaf 100644 --- a/packages/plugins/job-search/package.json +++ b/packages/plugins/job-search/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/job-search-plugin", + "name": "@gauzy/plugin-job-search", "version": "0.1.0", "description": "Ever Gauzy Platform Job Search Plugin", "author": { diff --git a/packages/plugins/job-search/src/employee-job/commands/get-employee-job-statistics.command.ts b/packages/plugins/job-search/src/employee-job/commands/get-employee-job-statistics.command.ts new file mode 100644 index 00000000000..c0ae63ea0ad --- /dev/null +++ b/packages/plugins/job-search/src/employee-job/commands/get-employee-job-statistics.command.ts @@ -0,0 +1,8 @@ +import { Employee, PaginationParams } from '@gauzy/core'; +import { ICommand } from '@nestjs/cqrs'; + +export class GetEmployeeJobStatisticsCommand implements ICommand { + static readonly type = '[EmployeeJobStatistics] Get'; + + constructor(public readonly options: PaginationParams) {} +} diff --git a/packages/core/src/employee/commands/handlers/get-employee-job-statistics.handler.ts b/packages/plugins/job-search/src/employee-job/commands/handlers/get-employee-job-statistics.handler.ts similarity index 84% rename from packages/core/src/employee/commands/handlers/get-employee-job-statistics.handler.ts rename to packages/plugins/job-search/src/employee-job/commands/handlers/get-employee-job-statistics.handler.ts index e3e367975dd..a62ce48a392 100644 --- a/packages/core/src/employee/commands/handlers/get-employee-job-statistics.handler.ts +++ b/packages/plugins/job-search/src/employee-job/commands/handlers/get-employee-job-statistics.handler.ts @@ -1,7 +1,7 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { GauzyAIService } from '@gauzy/integration-ai'; import { IEmployee, IPagination } from '@gauzy/contracts'; -import { EmployeeService } from '../../employee.service'; +import { EmployeeService } from '@gauzy/core'; import { GetEmployeeJobStatisticsCommand } from '../get-employee-job-statistics.command'; @CommandHandler(GetEmployeeJobStatisticsCommand) @@ -11,10 +11,7 @@ export class GetEmployeeJobStatisticsHandler implements ICommandHandler ({ ...employee, - ...employeesStatisticsById.get(employee.id) || {} // Use empty object if not found + ...(employeesStatisticsById.get(employee.id) || {}) // Use empty object if not found })); return { items, total }; diff --git a/packages/core/src/employee/commands/handlers/update-employee-job-search-status.handler.ts b/packages/plugins/job-search/src/employee-job/commands/handlers/update-employee-job-search-status.handler.ts similarity index 56% rename from packages/core/src/employee/commands/handlers/update-employee-job-search-status.handler.ts rename to packages/plugins/job-search/src/employee-job/commands/handlers/update-employee-job-search-status.handler.ts index a92421223b2..781e1f60b8f 100644 --- a/packages/core/src/employee/commands/handlers/update-employee-job-search-status.handler.ts +++ b/packages/plugins/job-search/src/employee-job/commands/handlers/update-employee-job-search-status.handler.ts @@ -2,44 +2,36 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { UpdateResult } from 'typeorm'; import { IEmployee } from '@gauzy/contracts'; import { GauzyAIService } from '@gauzy/integration-ai'; -import { RequestContext } from './../../../core/context'; -import { EmployeeService } from '../../employee.service'; +import { EmployeeService, RequestContext } from '@gauzy/core'; import { UpdateEmployeeJobSearchStatusCommand } from '../update-employee-job-search-status.command'; @CommandHandler(UpdateEmployeeJobSearchStatusCommand) export class UpdateEmployeeJobSearchStatusHandler implements ICommandHandler { - - constructor( - private readonly employeeService: EmployeeService, - private readonly gauzyAIService: GauzyAIService - ) { } - - public async execute( - command: UpdateEmployeeJobSearchStatusCommand - ): Promise { - + constructor(private readonly employeeService: EmployeeService, private readonly gauzyAIService: GauzyAIService) {} + + /** + * Executes the command to update an employee's job search status. + * + * @param command - The command containing the employee ID and input data. + * @returns A promise resolving to the updated employee or the update result. + */ + public async execute(command: UpdateEmployeeJobSearchStatusCommand): Promise { const { employeeId, input } = command; const { isJobSearchActive, organizationId } = input; const tenantId = RequestContext.currentTenantId() || input.tenantId; const employee = await this.employeeService.findOneByIdString(employeeId, { - where: { - organizationId, - tenantId - }, - relations: { - user: true, - organization: true - } + where: { organizationId, tenantId }, + relations: { user: true, organization: true } }); - try { // Attempt to sync the employee with Gauzy AI const syncResult = await this.gauzyAIService.syncEmployees([employee]); - try { - if (syncResult) { - const { userId } = employee; + + if (syncResult) { + const { userId } = employee; + try { await this.gauzyAIService.updateEmployeeStatus({ employeeId, userId, @@ -49,19 +41,20 @@ export class UpdateEmployeeJobSearchStatusHandler implements ICommandHandler Boolean }) + @IsBoolean() + isJobSearchActive: boolean; +} diff --git a/packages/plugins/job-search/src/employee-job/dto/index.ts b/packages/plugins/job-search/src/employee-job/dto/index.ts new file mode 100644 index 00000000000..a1cdadc9719 --- /dev/null +++ b/packages/plugins/job-search/src/employee-job/dto/index.ts @@ -0,0 +1 @@ +export * from './employee-job-statistic.dto'; diff --git a/packages/plugins/job-search/src/employee-job/employee-job.controller.ts b/packages/plugins/job-search/src/employee-job/employee-job.controller.ts index b2d7bd286c1..dee85ead157 100644 --- a/packages/plugins/job-search/src/employee-job/employee-job.controller.ts +++ b/packages/plugins/job-search/src/employee-job/employee-job.controller.ts @@ -1,25 +1,32 @@ -import { Controller, HttpStatus, Get, Query, Post, Body, Param } from '@nestjs/common'; +import { Controller, HttpStatus, Get, Query, Post, Body, Param, Put } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { CommandBus } from '@nestjs/cqrs'; +import { UpdateResult } from 'typeorm'; import { + ID, + IEmployee, IEmployeeJobApplication, IEmployeeJobApplicationAppliedResult, IEmployeeJobPost, IGetEmployeeJobPostInput, IPagination, IUpdateEmployeeJobPostAppliedResult, - IVisibilityJobPostInput + IVisibilityJobPostInput, + PermissionsEnum } from '@gauzy/contracts'; -import { UUIDValidationPipe, UseValidationPipe } from '@gauzy/core'; +import { UUIDValidationPipe, UseValidationPipe, Permissions, PaginationParams, Employee } from '@gauzy/core'; import { EmployeeJobPostService } from './employee-job.service'; import { EmployeeJobPost } from './employee-job.entity'; +import { GetEmployeeJobStatisticsCommand, UpdateEmployeeJobSearchStatusCommand } from './commands'; +import { EmployeeJobStatisticDTO } from './dto'; @ApiTags('EmployeeJobPost') @Controller('/employee-job') export class EmployeeJobPostController { - constructor( - private readonly _employeeJobPostService: EmployeeJobPostService - ) { } + private readonly _employeeJobPostService: EmployeeJobPostService, + private readonly _commandBus: CommandBus + ) {} /** * Find all employee job posts. @@ -37,13 +44,63 @@ export class EmployeeJobPostController { status: HttpStatus.NOT_FOUND, description: 'Record not found' }) - @Get() - async findAll( - @Query() input: IGetEmployeeJobPostInput - ): Promise> { + @Get('/') + async findAll(@Query() input: IGetEmployeeJobPostInput): Promise> { return await this._employeeJobPostService.findAll(input); } + /** + * GET employee job statistics. + * + * This endpoint retrieves statistics related to employee jobs, + * providing details about job distribution, assignments, or other related data. + * + * @param options Pagination parameters for retrieving the data. + * @returns A paginated list of employee job statistics. + */ + @ApiOperation({ summary: 'Retrieve employee job statistics' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Employee job statistics found' + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input. The response body may contain clues about what went wrong.' + }) + @Permissions(PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW) + @Get('/statistics') + @UseValidationPipe({ transform: true }) + async getEmployeeJobsStatistics(@Query() query: PaginationParams): Promise> { + return await this._commandBus.execute(new GetEmployeeJobStatisticsCommand(query)); + } + + /** + * UPDATE employee's job search status by their IDs + * + * This endpoint allows updating the job search status of an employee, given their ID. + * + * @param employeeId The unique identifier of the employee whose job search status is being updated. + * @param entity The updated job search status information. + * @returns A promise resolving to the updated employee record or an update result. + */ + @ApiOperation({ summary: 'Update Job Search Status' }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Job search status has been successfully updated.' + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input. The response body may contain clues as to what went wrong.' + }) + @Put('/:id/job-search-status') + @UseValidationPipe({ whitelist: true }) + async updateJobSearchStatus( + @Param('id', UUIDValidationPipe) employeeId: ID, + @Body() data: EmployeeJobStatisticDTO + ): Promise { + return await this._commandBus.execute(new UpdateEmployeeJobSearchStatusCommand(employeeId, data)); + } + /** * Apply for a job. * @@ -61,10 +118,8 @@ export class EmployeeJobPostController { description: 'Record not found' }) @UseValidationPipe() // Assuming ValidationPipe is configured appropriately - @Post('apply') - async apply( - @Body() input: IEmployeeJobApplication - ): Promise { + @Post('/apply') + async apply(@Body() input: IEmployeeJobApplication): Promise { try { // Apply for the job using the service const appliedJobPost = await this._employeeJobPostService.apply(input); @@ -97,10 +152,8 @@ export class EmployeeJobPostController { description: 'Record not found' }) @UseValidationPipe() // Assuming ValidationPipe is configured appropriately - @Post('updateApplied') - async updateApplied( - @Body() input: IEmployeeJobApplication - ): Promise { + @Post('/updateApplied') + async updateApplied(@Body() input: IEmployeeJobApplication): Promise { try { // Update the job application status using the service const updatedJobPost = await this._employeeJobPostService.updateApplied(input); @@ -134,10 +187,8 @@ export class EmployeeJobPostController { description: 'Record not found' }) @UseValidationPipe() // Assuming ValidationPipe is configured appropriately - @Post('hide') - async updateVisibility( - @Body() data: IVisibilityJobPostInput - ): Promise { + @Post('/hide') + async updateVisibility(@Body() data: IVisibilityJobPostInput): Promise { try { // Update the job visibility status using the service const updatedJobPost = await this._employeeJobPostService.updateVisibility(data); diff --git a/packages/plugins/job-search/src/employee-job/employee-job.module.ts b/packages/plugins/job-search/src/employee-job/employee-job.module.ts index 662742fb73a..b053ae36044 100644 --- a/packages/plugins/job-search/src/employee-job/employee-job.module.ts +++ b/packages/plugins/job-search/src/employee-job/employee-job.module.ts @@ -1,13 +1,14 @@ import { Module } from '@nestjs/common'; -import { EmployeeModule, IntegrationTenantModule } from '@gauzy/core'; +import { CqrsModule } from '@nestjs/cqrs'; import { GauzyAIModule } from '@gauzy/integration-ai'; +import { EmployeeModule, IntegrationTenantModule } from '@gauzy/core'; import { EmployeeJobPostService } from './employee-job.service'; import { EmployeeJobPostController } from './employee-job.controller'; +import { CommandHandlers } from './commands'; @Module({ - imports: [EmployeeModule, IntegrationTenantModule, GauzyAIModule.forRoot()], + imports: [EmployeeModule, IntegrationTenantModule, GauzyAIModule.forRoot(), CqrsModule], controllers: [EmployeeJobPostController], - providers: [EmployeeJobPostService], - exports: [] + providers: [EmployeeJobPostService, ...CommandHandlers] }) -export class EmployeeJobPostModule { } +export class EmployeeJobPostModule {} diff --git a/packages/plugins/job-search/src/job-search.plugin.ts b/packages/plugins/job-search/src/job-search.plugin.ts index 65fae9b0233..e5df24c3f2c 100644 --- a/packages/plugins/job-search/src/job-search.plugin.ts +++ b/packages/plugins/job-search/src/job-search.plugin.ts @@ -1,4 +1,3 @@ - import * as chalk from 'chalk'; import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable } from '@gauzy/plugin'; import { SeederModule } from '@gauzy/core'; @@ -14,7 +13,7 @@ import { JobSeederService } from './employee-job-preset/job-seeder.service'; configuration: (config: ApplicationPluginConfig) => { // Configuration object for custom fields in the Employee entity. config.customFields.Employee.push({ - propertyPath: 'jobPresets', + name: 'jobPresets', type: 'relation', relationType: 'many-to-many', pivotTable: 'employee_job_preset', @@ -28,11 +27,10 @@ import { JobSeederService } from './employee-job-preset/job-seeder.service'; providers: [JobSeederService] }) export class JobSearchPlugin implements IOnPluginBootstrap, IOnPluginDestroy, IOnPluginSeedable { - // We disable by default additional logging for each event to avoid cluttering the logs private logEnabled = true; - constructor(private readonly jobSeederService: JobSeederService) { } + constructor(private readonly jobSeederService: JobSeederService) {} /** * Called when the plugin is being initialized. diff --git a/packages/plugins/knowledge-base/package.json b/packages/plugins/knowledge-base/package.json index 0ab8fb2db36..15a08440ecf 100644 --- a/packages/plugins/knowledge-base/package.json +++ b/packages/plugins/knowledge-base/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/knowledge-base-plugin", + "name": "@gauzy/plugin-knowledge-base", "version": "0.1.0", "description": "Ever Gauzy Platform Knowledge Base plugin", "author": { diff --git a/packages/plugins/sentry-tracing/README.md b/packages/plugins/sentry-tracing/README.md index cc11b60919a..beeb0562cb4 100644 --- a/packages/plugins/sentry-tracing/README.md +++ b/packages/plugins/sentry-tracing/README.md @@ -15,7 +15,7 @@ The Gauzy Sentry Plugin seamlessly integrates your server with the [Sentry](http Install the Gauzy Sentry Plugin using your preferred package manager: ```bash -npm install @gauzy/sentry-plugin +npm install @gauzy/plugin-sentry # or -yarn add @gauzy/sentry-plugin +yarn add @gauzy/plugin-sentry ``` diff --git a/packages/plugins/sentry-tracing/package.json b/packages/plugins/sentry-tracing/package.json index 40dc0d0e90d..9e4f209d699 100644 --- a/packages/plugins/sentry-tracing/package.json +++ b/packages/plugins/sentry-tracing/package.json @@ -1,5 +1,5 @@ { - "name": "@gauzy/sentry-plugin", + "name": "@gauzy/plugin-sentry", "version": "0.1.0", "description": "Gauzy Sentry Plugin - Seamless integration with Sentry for advanced error tracking and monitoring in Gauzy Platform.", "author": { @@ -38,6 +38,7 @@ "rxjs": "^7.4.0" }, "devDependencies": { + "@types/jest": "^29.4.4", "@types/node": "^20.14.9", "rimraf": "^3.0.2", "typescript": "5.1.6" diff --git a/packages/ui-core/package.json b/packages/ui-core/package.json index 49439ea85e9..ed526a722b6 100644 --- a/packages/ui-core/package.json +++ b/packages/ui-core/package.json @@ -76,7 +76,8 @@ "date-holidays": "^1.6.1", "echarts": "^5.0.1", "eva-icons": "^1.1.3", - "ckeditor4-angular": "^5.1.0", + "ckeditor4": "4.22.1", + "ckeditor4-angular": "4.0.1", "file-saver": "^2.0.5", "fullcalendar": "^6.1.8", "hotkeys-js": "^3.12.0", @@ -122,6 +123,7 @@ }, "sideEffects": false, "devDependencies": { + "@types/ckeditor": "^4.9.10", "@types/jest": "^29.4.4", "jest": "^29.7.0", "jest-preset-angular": "^13.1.4" diff --git a/packages/ui-core/src/lib/core/src/interceptors/api.interceptor.ts b/packages/ui-core/src/lib/core/src/interceptors/api.interceptor.ts index 47579214a6e..4f98a2fc704 100644 --- a/packages/ui-core/src/lib/core/src/interceptors/api.interceptor.ts +++ b/packages/ui-core/src/lib/core/src/interceptors/api.interceptor.ts @@ -19,10 +19,8 @@ export class APIInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable> { if (baseUrl && request.url.startsWith(API_PREFIX)) { const url = baseUrl + request.url; - // console.log(`API Request: ${request.url} -> ${url}`); - request = request.clone({ - url: url - }); + console.log(`API Request: ${request.url} -> ${url}`); + request = request.clone({ url }); } return next.handle(request); } diff --git a/packages/ui-core/src/lib/core/src/services/auth/auth.service.ts b/packages/ui-core/src/lib/core/src/services/auth/auth.service.ts index c157196061e..f77bb833639 100644 --- a/packages/ui-core/src/lib/core/src/services/auth/auth.service.ts +++ b/packages/ui-core/src/lib/core/src/services/auth/auth.service.ts @@ -103,8 +103,8 @@ export class AuthService { return this.http.get(`${API_PREFIX}/auth/logout`); } - register(registerInput: IUserRegistrationInput): Observable { - return this.http.post(`${API_PREFIX}/auth/register`, registerInput); + register(input: IUserRegistrationInput): Observable { + return this.http.post(`${API_PREFIX}/auth/register`, input); } requestPassword(requestPasswordInput): Observable<{ id?: string; token?: string }> { diff --git a/packages/ui-core/src/lib/core/src/services/deals/deals.service.ts b/packages/ui-core/src/lib/core/src/services/deals/deals.service.ts index 2ef0d13a4f8..1ca150f9e15 100644 --- a/packages/ui-core/src/lib/core/src/services/deals/deals.service.ts +++ b/packages/ui-core/src/lib/core/src/services/deals/deals.service.ts @@ -1,23 +1,44 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { IDeal, IDealCreateInput, IDealFindInput } from '@gauzy/contracts'; -import { API_PREFIX } from '@gauzy/ui-core/common'; import { firstValueFrom } from 'rxjs'; +import { ID, IDeal, IDealCreateInput, IDealFindInput, IPagination } from '@gauzy/contracts'; +import { API_PREFIX, toParams } from '@gauzy/ui-core/common'; import { Service } from '../crud/service'; @Injectable() export class DealsService extends Service { - public constructor(protected http: HttpClient) { + constructor(readonly http: HttpClient) { super({ http, basePath: `${API_PREFIX}/deals` }); } - getAll(findInput?: IDealFindInput, relations?: string[]): Promise { - const data = JSON.stringify({ relations, findInput }); - return firstValueFrom(this.http.get(`${this.basePath}`, { params: { data } })); + /** + * Fetch all deals with optional relations and filter conditions + * + * @param relations Array of relation names to include in the result + * @param where Filter conditions for fetching deals + * @returns A promise of paginated deals + */ + getAll(relations?: string[], where?: IDealFindInput): Promise> { + return firstValueFrom( + this.http.get>(`${this.basePath}`, { + params: toParams({ where, relations }) + }) + ); } - getOne(id: string, findInput?: IDealFindInput, relations?: string[]): Promise { - const data = JSON.stringify({ relations, findInput }); - return firstValueFrom(this.http.get(`${this.basePath}/${id}`, { params: { data } })); + /** + * Fetch a deal by its ID with optional relations and filter conditions + * + * @param id The ID of the deal to fetch + * @param where Filter conditions for fetching the deal + * @param relations Array of relation names to include in the result + * @returns A promise of the fetched deal + */ + getById(id: ID, where?: IDealFindInput, relations: string[] = []): Promise { + return firstValueFrom( + this.http.get(`${this.basePath}/${id}`, { + params: toParams({ where, relations }) + }) + ); } } diff --git a/packages/ui-core/src/lib/core/src/services/employee/employee-store.service.ts b/packages/ui-core/src/lib/core/src/services/employee/employee-store.service.ts index 2967a210d80..24c13b8d111 100644 --- a/packages/ui-core/src/lib/core/src/services/employee/employee-store.service.ts +++ b/packages/ui-core/src/lib/core/src/services/employee/employee-store.service.ts @@ -33,22 +33,13 @@ export class EmployeeStore { private _userForm: IUserFindInput; private _employeeForm: IEmployeeUpdateInput; - constructor( - protected employeeAkitaStore: EmployeeAkitaStore, - protected employeeAkitaQuery: EmployeeAkitaQuery - ) { } + constructor(protected employeeAkitaStore: EmployeeAkitaStore, protected employeeAkitaQuery: EmployeeAkitaQuery) {} - selectedEmployee$: BehaviorSubject = new BehaviorSubject( - this.selectedEmployee - ); + selectedEmployee$: BehaviorSubject = new BehaviorSubject(this.selectedEmployee); - userForm$: BehaviorSubject = new BehaviorSubject( - this.userForm - ); + userForm$: BehaviorSubject = new BehaviorSubject(this.userForm); - employeeForm$: BehaviorSubject = new BehaviorSubject( - this.employeeForm - ); + employeeForm$: BehaviorSubject = new BehaviorSubject(this.employeeForm); set selectedEmployee(employee: IEmployee) { this._selectedEmployee = employee; @@ -88,6 +79,28 @@ export class EmployeeStore { return this._employeeForm; } + /** + * Update the user form with new data + * + * @param formData - The form data to update. + */ + async updateUserForm(formData: IUserUpdateInput) { + // Simulate an async operation, such as an API call + // await someApiService.update(formData); + this.userForm = { ...this.userForm, ...formData }; + } + + /** + * Update the employee form with new data + * + * @param formData - The form data to update. + */ + async updateEmployeeForm(formData: IEmployeeUpdateInput) { + // Simulate an async operation, such as an API call + // await someApiService.updateEmployee(formData); + this.employeeForm = { ...this.employeeForm, ...formData }; + } + destroy() { this.employeeAkitaStore.reset(); } diff --git a/packages/ui-core/src/lib/core/src/services/employee/employees.service.ts b/packages/ui-core/src/lib/core/src/services/employee/employees.service.ts index 2edf182591c..4eedf6f702d 100644 --- a/packages/ui-core/src/lib/core/src/services/employee/employees.service.ts +++ b/packages/ui-core/src/lib/core/src/services/employee/employees.service.ts @@ -10,7 +10,7 @@ import { IBasePerTenantAndOrganizationEntityModel, IDateRangePicker, IPagination, - UpdateEmployeeJobsStatistics + ID } from '@gauzy/contracts'; import { API_PREFIX, toParams } from '@gauzy/ui-core/common'; @@ -24,7 +24,7 @@ export class EmployeesService { }); } - getPublicById(slug: string, id: string, relations: string[] = []): Observable { + getPublicById(slug: string, id: ID, relations: string[] = []): Observable { return this.http.get(`${API_PREFIX}/public/employee/${slug}/${id}`, { params: toParams({ relations }) }); @@ -62,7 +62,7 @@ export class EmployeesService { ); } - getWorkingCount(organizationId: string, tenantId: string, forRange: IDateRangePicker): Promise<{ total: number }> { + getWorkingCount(organizationId: ID, tenantId: ID, forRange: IDateRangePicker): Promise<{ total: number }> { const query = { organizationId, tenantId, @@ -76,17 +76,39 @@ export class EmployeesService { ); } - getEmployeeById(id: string, relations: string[] = []) { + /** + * Retrieves employee information by ID. + * + * @param id - The ID of the employee. + * @param relations - Optional array of relations to include in the response. + * @returns An observable of type `IEmployee` containing the employee's information. + */ + getEmployeeById(id: ID, relations: string[] = []) { return this.http.get(`${API_PREFIX}/employee/${id}`, { params: toParams({ relations }) }); } - setEmployeeProfileStatus(id: string, status: IEmployeeUpdateProfileStatus): Promise { + /** + * Updates the profile status of an employee. + * + * @param id - The ID of the employee. + * @param status - The new profile status to set for the employee. + * @returns A promise that resolves with the updated employee object. + */ + setEmployeeProfileStatus(id: ID, status: IEmployeeUpdateProfileStatus): Promise { return firstValueFrom(this.http.put(`${API_PREFIX}/employee/${id}`, status)); } - setEmployeeEndWork(id: string, date: Date, request: IBasePerTenantAndOrganizationEntityModel): Promise { + /** + * Sets the end work date for an employee. + * + * @param id - The ID of the employee. + * @param date - The date when the employee's work ended. + * @param request - Additional data related to the employee (e.g., tenant and organization). + * @returns A promise that resolves with the updated employee object. + */ + setEmployeeEndWork(id: ID, date: Date, request: IBasePerTenantAndOrganizationEntityModel): Promise { return firstValueFrom( this.http.put(`${API_PREFIX}/employee/${id}`, { endWork: date, @@ -95,8 +117,16 @@ export class EmployeesService { ); } + /** + * Updates the time tracking status for an employee. + * + * @param id - The ID of the employee. + * @param action - Boolean indicating whether to enable or disable time tracking. + * @param request - Additional data related to the employee (e.g., tenant and organization). + * @returns A promise that resolves with the updated employee object. + */ setEmployeeTimeTrackingStatus( - id: string, + id: ID, action: boolean, request: IBasePerTenantAndOrganizationEntityModel ): Promise { @@ -108,7 +138,14 @@ export class EmployeesService { ); } - update(id: string, updateInput: IEmployeeUpdateInput): Promise { + /** + * Updates an employee's information. + * + * @param id - The ID of the employee to update. + * @param updateInput - The data to update for the employee. + * @returns A promise that resolves when the update operation is completed. + */ + update(id: ID, updateInput: IEmployeeUpdateInput): Promise { return firstValueFrom(this.http.put(`${API_PREFIX}/employee/${id}`, updateInput)); } @@ -121,7 +158,7 @@ export class EmployeesService { * @param options - Additional context for the operation, including tenant and organization information. * @returns A promise resolving to the result of the DELETE operation or an error message. */ - delete(id: string, options: IBasePerTenantAndOrganizationEntityModel): Promise { + delete(id: ID, options: IBasePerTenantAndOrganizationEntityModel): Promise { return firstValueFrom( this.http.delete(`${API_PREFIX}/employee/${id}`, { params: toParams({ ...options }) @@ -136,7 +173,7 @@ export class EmployeesService { * @param options - Additional options for specifying tenant and organization context. * @returns A promise resolving to the deleted employee or a success indicator. */ - softRemove(id: string, options: IBasePerTenantAndOrganizationEntityModel): Promise { + softRemove(id: ID, options: IBasePerTenantAndOrganizationEntityModel): Promise { return firstValueFrom( this.http.delete(`${API_PREFIX}/employee/${id}/soft`, { params: toParams({ ...options }) @@ -151,56 +188,50 @@ export class EmployeesService { * @param options - Additional context, typically to specify tenant and organization information. * @returns A promise resolving to the restored employee or a success indicator. */ - softRecover(id: string, options: IBasePerTenantAndOrganizationEntityModel): Promise { + softRecover(id: ID, options: IBasePerTenantAndOrganizationEntityModel): Promise { return firstValueFrom(this.http.put(`${API_PREFIX}/employee/${id}/recover`, { ...options })); } /** + * Updates the profile of an employee. * - * @param id - * @param payload - * @returns + * @param id - The ID of the employee. + * @param payload - The data to update in the employee's profile. + * @returns A promise that resolves with the updated employee profile. */ - updateProfile(id: string, payload: IEmployeeUpdateInput): Promise { + updateProfile(id: ID, payload: IEmployeeUpdateInput): Promise { return firstValueFrom(this.http.put(`${API_PREFIX}/employee/${id}/profile`, payload)); } /** + * Creates a new employee with the provided input data. * - * @param request - * @returns + * @param input - The data to create a new employee. + * @returns An observable of the created employee. */ - getEmployeeJobsStatistics(request): Promise { - return firstValueFrom( - this.http.get(`${API_PREFIX}/employee/job-statistics`, { - params: toParams(request) - }) - ); + create(input: IEmployeeCreateInput): Observable { + return this.http.post(`${API_PREFIX}/employee`, input); } /** + * Creates multiple new employees with the provided input data. * - * @param id - * @param statistics - * @returns + * @param createInput - An array of objects, each containing the data to create a new employee. + * @returns An observable of an array of created employees. */ - updateJobSearchStatus(id: IEmployee['id'], statistics: UpdateEmployeeJobsStatistics) { - return firstValueFrom(this.http.put(`${API_PREFIX}/employee/${id}/job-search-status`, statistics)); + createBulk(input: IEmployeeCreateInput[]): Observable { + return this.http.post(`${API_PREFIX}/employee/bulk`, input); } - create(body: IEmployeeCreateInput): Observable { - return this.http.post(`${API_PREFIX}/employee`, body); - } - - createBulk(createInput: IEmployeeCreateInput[]): Observable { - return this.http.post(`${API_PREFIX}/employee/bulk`, createInput); - } - - setEmployeeStartWork( - id: string, - date: Date, - request: IBasePerTenantAndOrganizationEntityModel - ): Promise { + /** + * Sets the start work date for an employee. + * + * @param id - The ID of the employee. + * @param date - The date when the employee started work. + * @param request - Additional request data that includes tenant and organization details. + * @returns A promise of the updated employee. + */ + setEmployeeStartWork(id: ID, date: Date, request: IBasePerTenantAndOrganizationEntityModel): Promise { return firstValueFrom( this.http.put(`${API_PREFIX}/employee/${id}`, { startedWorkOn: date, diff --git a/packages/ui-core/src/lib/core/src/services/hubstaff/hubstaff.service.ts b/packages/ui-core/src/lib/core/src/services/hubstaff/hubstaff.service.ts index a480c84a8ea..e025744179c 100644 --- a/packages/ui-core/src/lib/core/src/services/hubstaff/hubstaff.service.ts +++ b/packages/ui-core/src/lib/core/src/services/hubstaff/hubstaff.service.ts @@ -16,10 +16,8 @@ import { IDateRangeActivityFilter, IEntitySettingToSync } from '@gauzy/contracts'; -import { toParams } from '@gauzy/ui-core/common'; -import { HUBSTAFF_AUTHORIZATION_URL } from '@gauzy/integration-hubstaff'; +import { API_PREFIX, toParams } from '@gauzy/ui-core/common'; import { environment } from '@gauzy/ui-config'; -import { API_PREFIX } from '@gauzy/ui-core/common'; const TODAY = new Date(); @@ -102,9 +100,11 @@ export class HubstaffService { * @param client_id The client ID for the Hubstaff integration. */ authorizeClient(client_id: string): void { - const redirect_uri = - environment.HUBSTAFF_REDIRECT_URL || - `${environment.API_BASE_URL}${API_PREFIX}/integration/hubstaff/callback`; + const { HUBSTAFF_REDIRECT_URL, API_BASE_URL } = environment; + const HUBSTAFF_AUTHORIZATION_URL = `https://account.hubstaff.com`; + + // Set default redirect URI if HUBSTAFF_REDIRECT_URL is not defined + const redirect_uri = HUBSTAFF_REDIRECT_URL || `${API_BASE_URL}${API_PREFIX}/integration/hubstaff/callback`; // Define your query parameters const queryParams = toParams({ @@ -124,10 +124,17 @@ export class HubstaffService { window.location.replace(externalUrl); } + /** + * Add a new integration for Hubstaff. + * + * @param param0 - The integration parameters including code, client_secret, client_id, and organizationId. + * @returns An Observable of the created integration tenant. + */ addIntegration({ code, client_secret, client_id, organizationId }): Observable { - const redirect_uri = - environment.HUBSTAFF_REDIRECT_URL || - `${environment.API_BASE_URL}${API_PREFIX}/integration/hubstaff/callback`; + const { HUBSTAFF_REDIRECT_URL, API_BASE_URL } = environment; + + // Set default redirect URI if HUBSTAFF_REDIRECT_URL is not defined + const redirect_uri = HUBSTAFF_REDIRECT_URL || `${API_BASE_URL}${API_PREFIX}/integration/hubstaff/callback`; return this._http.post(`${API_PREFIX}/integration/hubstaff/integration`, { client_id, diff --git a/packages/ui-core/src/lib/core/src/services/job/job.service.ts b/packages/ui-core/src/lib/core/src/services/job/job.service.ts index a4b0ecbebb5..4b5302d214d 100644 --- a/packages/ui-core/src/lib/core/src/services/job/job.service.ts +++ b/packages/ui-core/src/lib/core/src/services/job/job.service.ts @@ -8,18 +8,25 @@ import { IEmployeeJobApplicationAppliedResult, IUpdateEmployeeJobPostAppliedResult, IEmployeeJobApplication, - IVisibilityJobPostInput + IVisibilityJobPostInput, + ID, + UpdateEmployeeJobsStatistics } from '@gauzy/contracts'; -import { toParams } from '@gauzy/ui-core/common'; -import { API_PREFIX } from '@gauzy/ui-core/common'; +import { API_PREFIX, toParams } from '@gauzy/ui-core/common'; @Injectable({ providedIn: 'root' }) export class JobService { - constructor(private http: HttpClient) {} + constructor(private readonly http: HttpClient) {} - getJobs(request?: IGetEmployeeJobPostInput) { + /** + * Fetches job posts based on the given request parameters. + * + * @param request - An optional object of type IGetEmployeeJobPostInput containing filter parameters for fetching job posts. + * @returns A promise that resolves to an IPagination object containing the paginated job posts. + */ + getJobs(request?: IGetEmployeeJobPostInput): Promise> { return firstValueFrom( this.http.get>(`${API_PREFIX}/employee-job`, { params: request ? toParams(request) : {} @@ -27,17 +34,60 @@ export class JobService { ); } - hideJob(request: IVisibilityJobPostInput) { + /** + * Retrieves job statistics for employees based on the provided request parameters. + * + * @param request - Parameters for filtering and retrieving job statistics. + * @returns A promise that resolves with the job statistics data. + */ + getEmployeeJobsStatistics(request: any): Promise { + return firstValueFrom( + this.http.get(`${API_PREFIX}/employee-job/statistics`, { + params: toParams(request) + }) + ); + } + + /** + * Updates the job search status and statistics for an employee. + * + * @param id - The ID of the employee. + * @param statistics - An object containing job search status and statistics to be updated. + * @returns A promise that resolves with the updated employee's job search status and statistics. + */ + updateJobSearchStatus(id: ID, statistics: UpdateEmployeeJobsStatistics): Promise { + return firstValueFrom(this.http.put(`${API_PREFIX}/employee-job/${id}/job-search-status`, statistics)); + } + + /** + * Hides a job post based on the given request parameters. + * + * @param request - An object of type IVisibilityJobPostInput containing the necessary parameters to hide a job post. + * @returns A promise that resolves to a boolean indicating whether the job post was successfully hidden. + */ + hideJob(request: IVisibilityJobPostInput): Promise { return firstValueFrom(this.http.post(`${API_PREFIX}/employee-job/hide`, request)); } - updateApplied(request: IEmployeeJobApplication) { + /** + * Updates the application status of a job post. + * + * @param request - An object of type IEmployeeJobApplication containing the necessary parameters to update the application status of a job post. + * @returns A promise that resolves to an IUpdateEmployeeJobPostAppliedResult object containing the result of the update operation. + */ + updateApplied(request: IEmployeeJobApplication): Promise { return firstValueFrom( this.http.post(`${API_PREFIX}/employee-job/updateApplied`, request) ); } - applyJob(request: IEmployeeJobApplication) { + /** + * Applies for a job post based on the given request parameters. + * + * @param request - An object of type IEmployeeJobApplication containing the necessary parameters to apply for a job post. + * @returns A promise that resolves to an IEmployeeJobApplicationAppliedResult object containing the result of the application operation. + */ + applyJob(request: IEmployeeJobApplication): Promise { return firstValueFrom( this.http.post(`${API_PREFIX}/employee-job/apply`, request) ); diff --git a/packages/ui-core/src/lib/core/src/services/organization/organization-projects.service.ts b/packages/ui-core/src/lib/core/src/services/organization/organization-projects.service.ts index 0c5c45002de..e06401be5d4 100644 --- a/packages/ui-core/src/lib/core/src/services/organization/organization-projects.service.ts +++ b/packages/ui-core/src/lib/core/src/services/organization/organization-projects.service.ts @@ -9,7 +9,8 @@ import { IPagination, IEmployee, IOrganizationProjectUpdateInput, - IOrganizationProjectSetting + IOrganizationProjectSetting, + ID } from '@gauzy/contracts'; import { toParams } from '@gauzy/ui-core/common'; import { API_PREFIX } from '@gauzy/ui-core/common'; @@ -49,7 +50,7 @@ export class OrganizationProjectsService { ); } - getById(id: IOrganizationProject['id'], relations: string[] = []): Observable { + getById(id: ID, relations: string[] = []): Observable { return this._http.get(`${this.API_URL}/${id}`, { params: toParams({ relations }) }); @@ -67,16 +68,13 @@ export class OrganizationProjectsService { return firstValueFrom(this._http.put(`${this.API_URL}/employee`, updateInput)); } - updateTaskViewMode( - id: IOrganizationProject['id'], - body: IOrganizationProjectUpdateInput - ): Promise { + updateTaskViewMode(id: ID, body: IOrganizationProjectUpdateInput): Promise { return firstValueFrom( this._http.put(`${this.API_URL}/task-view/${id}`, body).pipe(take(1)) ); } - delete(id: IOrganizationProject['id']): Promise { + delete(id: ID): Promise { return firstValueFrom(this._http.delete(`${this.API_URL}/${id}`)); } @@ -88,11 +86,11 @@ export class OrganizationProjectsService { * * @returns An Observable of type `IOrganizationProject` representing the updated organization project. */ - updateProjectSetting( - id: IOrganizationProject['id'], - input: IOrganizationProjectSetting - ): Observable { + updateProjectSetting(id: ID, input: IOrganizationProjectSetting): Observable { + // Construct the URL for the API endpoint const url = `${this.API_URL}/setting/${id}`; + + // Send an HTTP Put request to the specified URL with input parameters return this._http.put(url, input); } diff --git a/packages/ui-core/src/lib/core/src/services/pipeline/pipelines.service.ts b/packages/ui-core/src/lib/core/src/services/pipeline/pipelines.service.ts index 56e0ad29dc3..0c0fee15f93 100644 --- a/packages/ui-core/src/lib/core/src/services/pipeline/pipelines.service.ts +++ b/packages/ui-core/src/lib/core/src/services/pipeline/pipelines.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { firstValueFrom } from 'rxjs'; -import { IDeal, IPagination, IPipeline, IPipelineCreateInput, IPipelineFindInput } from '@gauzy/contracts'; -import { API_PREFIX, Store } from '@gauzy/ui-core/common'; +import { firstValueFrom, Observable } from 'rxjs'; +import { ID, IDeal, IPagination, IPipeline, IPipelineCreateInput, IPipelineFindInput } from '@gauzy/contracts'; +import { API_PREFIX, Store, toParams } from '@gauzy/ui-core/common'; import { Service } from '../crud/service'; @Injectable() @@ -11,17 +11,46 @@ export class PipelinesService extends Service> { - const data = JSON.stringify({ relations, findInput }); + /** + * Fetches all pipelines with optional relations and filtering conditions. + * + * @param relations - An optional array of relation names to include in the response. + * @param where - Optional filtering conditions. + * @returns A promise that resolves with the paginated pipelines. + */ + getAll(relations?: string[], where?: IPipelineFindInput): Promise> { return firstValueFrom( this.http.get>(`${this.basePath}`, { - params: { data } + params: toParams({ where, relations }) }) ); } - public findDeals(id: string, findInput?: IPipelineFindInput): Promise> { - const data = JSON.stringify({ findInput }); - return firstValueFrom(this.http.get>(`${this.basePath}/${id}/deals`, { params: { data } })); + /** + * Fetches a pipeline by its ID with optional relations. + * + * @param id - The ID of the pipeline to fetch. + * @param relations - An array of relation names to include in the response. + * @returns A promise that resolves with the pipeline. + */ + getById(id: ID, where?: IPipelineFindInput, relations: string[] = []): Observable { + return this.http.get(`${this.basePath}/${id}`, { + params: toParams({ where, relations }) + }); + } + + /** + * Find deals associated with a specific pipeline + * + * @param pipelineId The ID of the pipeline + * @param where Filter conditions for fetching the deals + * @returns A promise of paginated deals + */ + getPipelineDeals(pipelineId: ID, where?: IPipelineFindInput): Promise> { + return firstValueFrom( + this.http.get>(`${this.basePath}/${pipelineId}/deals`, { + params: toParams({ where }) + }) + ); } } diff --git a/packages/ui-core/src/lib/core/src/services/users/users-organizations.service.ts b/packages/ui-core/src/lib/core/src/services/users/users-organizations.service.ts index 79e6583b8aa..974168f9eec 100644 --- a/packages/ui-core/src/lib/core/src/services/users/users-organizations.service.ts +++ b/packages/ui-core/src/lib/core/src/services/users/users-organizations.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom, Observable } from 'rxjs'; import { + ID, IPagination, IUserOrganization, IUserOrganizationCreateInput, @@ -37,7 +38,13 @@ export class UsersOrganizationsService { ); } - setUserAsInactive(id: string): Promise { + /** + * Set user as inactive in the organization. + * + * @param id - The ID of the user organization. + * @returns A promise that resolves to the updated user organization. + */ + setUserAsInactive(id: ID): Promise { return firstValueFrom( this.http.put(`${API_PREFIX}/user-organization/${id}`, { isActive: false @@ -45,15 +52,33 @@ export class UsersOrganizationsService { ); } - getUserOrganizationCount(id: string): Promise { - return firstValueFrom(this.http.get(`${API_PREFIX}/user-organization/${id}`)); + /** + * Get the count of organizations a user belongs to. + * + * @param id - The user ID. + * @returns A promise that resolves to the count of organizations. + */ + getUserOrganizationCount(id: ID): Promise { + return firstValueFrom(this.http.get(`${API_PREFIX}/user-organization/${id}/count`)); } - removeUserFromOrg(id: string): Promise { + /** + * Remove user from the organization. + * + * @param id - The ID of the user organization. + * @returns A promise that resolves to the removed user organization. + */ + removeUserFromOrg(id: ID): Promise { return firstValueFrom(this.http.delete(`${API_PREFIX}/user-organization/${id}`)); } - create(createInput: IUserOrganizationCreateInput): Observable { - return this.http.post(`${API_PREFIX}/user-organization`, createInput); + /** + * Create a new user organization. + * + * @param input - The input data for creating a user organization. + * @returns An observable that resolves to the created user organization. + */ + create(input: IUserOrganizationCreateInput): Observable { + return this.http.post(`${API_PREFIX}/user-organization`, input); } } diff --git a/packages/ui-core/src/lib/package.json b/packages/ui-core/src/lib/package.json index e11d78053eb..5b44ab45549 100644 --- a/packages/ui-core/src/lib/package.json +++ b/packages/ui-core/src/lib/package.json @@ -10,7 +10,8 @@ "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", "angular2-smart-table": "^3.2.0", - "ckeditor4-angular": "^5.1.0", + "ckeditor4": "4.22.1", + "ckeditor4-angular": "4.0.1", "ngx-permissions": "^13.0.1", "rxjs": "^7.4.0", "tslib": "^2.6.2" diff --git a/packages/ui-core/src/lib/shared/src/directives/debounce-click.directive.ts b/packages/ui-core/src/lib/shared/src/directives/debounce-click.directive.ts index 1047adcdd64..9caaab9c568 100644 --- a/packages/ui-core/src/lib/shared/src/directives/debounce-click.directive.ts +++ b/packages/ui-core/src/lib/shared/src/directives/debounce-click.directive.ts @@ -12,15 +12,6 @@ export class DebounceClickDirective implements OnInit, OnDestroy { @Input() debounceTime = 300; @Output() throttledClick = new EventEmitter(); - ngOnInit() { - this.subscription = this.clicks - .pipe( - debounceTime(this.debounceTime), - tap((e) => this.throttledClick.emit(e)) - ) - .subscribe(); - } - /** * Handles the click event and emits it after a debounce time. * @@ -34,6 +25,15 @@ export class DebounceClickDirective implements OnInit, OnDestroy { this.clicks.next(event); } + ngOnInit() { + this.subscription = this.clicks + .pipe( + debounceTime(this.debounceTime), + tap((e) => this.throttledClick.emit(e)) + ) + .subscribe(); + } + ngOnDestroy() { this.subscription.unsubscribe(); } diff --git a/packages/ui-core/src/lib/shared/src/directives/directives.module.ts b/packages/ui-core/src/lib/shared/src/directives/directives.module.ts index 4c976d5e270..54c23b5a37b 100644 --- a/packages/ui-core/src/lib/shared/src/directives/directives.module.ts +++ b/packages/ui-core/src/lib/shared/src/directives/directives.module.ts @@ -1,10 +1,10 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { DIRECTIVES } from "./index"; +import { DIRECTIVES } from './index'; @NgModule({ + imports: [CommonModule], declarations: [...DIRECTIVES], - exports: [...DIRECTIVES], - imports: [CommonModule] + exports: [...DIRECTIVES] }) -export class DirectivesModule { } +export class DirectivesModule {} diff --git a/packages/ui-core/src/lib/shared/src/employee/employee-mutation/employee-mutation.component.ts b/packages/ui-core/src/lib/shared/src/employee/employee-mutation/employee-mutation.component.ts index 58b629f6cfc..fb9ec31efb6 100644 --- a/packages/ui-core/src/lib/shared/src/employee/employee-mutation/employee-mutation.component.ts +++ b/packages/ui-core/src/lib/shared/src/employee/employee-mutation/employee-mutation.component.ts @@ -15,16 +15,13 @@ import { BasicInfoFormComponent } from '../../user/forms'; styleUrls: ['employee-mutation.component.scss'] }) export class EmployeeMutationComponent implements OnInit, AfterViewInit { - @ViewChild('userBasicInfo') - userBasicInfo: BasicInfoFormComponent; - - @ViewChild('stepper') - stepper: NbStepperComponent; + @ViewChild('userBasicInfo') userBasicInfo: BasicInfoFormComponent; + @ViewChild('stepper') stepper: NbStepperComponent; loading: boolean = false; linear: boolean = true; form: UntypedFormGroup; - employees: IEmployeeCreateInput[] = []; + public employees: IEmployeeCreateInput[] = []; public organization: IOrganization; constructor( @@ -51,25 +48,33 @@ export class EmployeeMutationComponent implements OnInit, AfterViewInit { this.form = this.userBasicInfo.form; } - closeDialog(employee: IEmployee[] = null) { + /** + * Closes the dialog window. + * + * @param employee An optional array of employees to pass back to the caller. + */ + closeDialog(employee: IEmployee[] | null = null): void { this.dialogRef.close(employee); } - addEmployee() { + /** + * Adds an employee to the employees array based on form input. + * Resets the form and stepper after adding the employee. + */ + addEmployee(): void { + // Ensure organization is defined if (!this.organization) { return; } - const { id: organizationId } = this.organization; - const { tenantId } = this.store.user; - this.form = this.userBasicInfo.form; + // Extract necessary data from organization and user store + const { id: organizationId, tenantId } = this.organization; + + // Retrieve form values const { firstName, lastName, email, username, password, tags, imageUrl, imageId } = this.form.value; - const { - offerDate = null, - acceptDate = null, - rejectDate = null, - startedWorkOn = null - } = this.form.getRawValue(); + const { offerDate = null, acceptDate = null, rejectDate = null, startedWorkOn = null } = this.form.value; + + // Prepare user object const user: IUser = { firstName, lastName, @@ -80,39 +85,60 @@ export class EmployeeMutationComponent implements OnInit, AfterViewInit { tenantId, tags }; + + // Prepare employee input object const employee: IEmployeeCreateInput = { user, startedWorkOn, password, organizationId, + organization: { id: organizationId }, offerDate, acceptDate, rejectDate, tags }; - // Check form validity before to add an employee to the array of employees. - if (this.form.valid) this.employees.push(employee); - // Reset form and stepper. + + // Add employee to the array if form is valid + if (this.form.valid) { + this.employees.push(employee); + } + + // Reset form and stepper after adding employee this.form.reset(); this.stepper.reset(); } + /** + * Adds multiple employees and handles the process of creation. + * Closes the dialog upon successful creation or handles errors. + */ async add() { + // Check if organization is defined if (!this.organization) { return; } + + // Add employee based on form input this.addEmployee(); + try { + // Set loading state to true this.loading = true; - const employees = await firstValueFrom(this.employeesService.createBulk(this.employees)).finally(() => { - this.loading = false; - }); + // Create employees in bulk using service + const employees: IEmployee[] = await firstValueFrom(this.employeesService.createBulk(this.employees)); + this.loading = false; // Set loading state to false regardless of success or failure + + // Update employee action in store this._employeeStore.employeeAction = { action: CrudActionEnum.CREATED, employees }; + + // Close dialog with created employees this.closeDialog(employees); } catch (error) { + // Handle errors using error handler service this.errorHandler.handleError(error); } } diff --git a/packages/ui-core/src/lib/shared/src/file-uploader-input/file-uploader-input.component.ts b/packages/ui-core/src/lib/shared/src/file-uploader-input/file-uploader-input.component.ts index 2e180c6eef7..08a577b5083 100644 --- a/packages/ui-core/src/lib/shared/src/file-uploader-input/file-uploader-input.component.ts +++ b/packages/ui-core/src/lib/shared/src/file-uploader-input/file-uploader-input.component.ts @@ -60,6 +60,9 @@ export class FileUploaderInputComponent extends ImageUploaderBaseComponent imple ngOnInit(): void {} ngAfterViewInit(): void { + this.uploader.onAfterAddingFile = (file) => { + file.withCredentials = false; + }; this.uploader.onSuccessItem = (item: any, response: string, status: number) => { try { if (response) { diff --git a/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader-base.component.ts b/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader-base.component.ts index 6ea98077f2b..9bc6c289f51 100644 --- a/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader-base.component.ts +++ b/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader-base.component.ts @@ -1,20 +1,20 @@ import { Component, Input } from '@angular/core'; -import { IUser } from '@gauzy/contracts'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { FileUploader, FileUploaderOptions } from 'ng2-file-upload'; +import { FileItem, FileUploader, FileUploaderOptions } from 'ng2-file-upload'; import { Subject } from 'rxjs/internal/Subject'; import { filter, tap } from 'rxjs/operators'; +import { IOrganization, IUser } from '@gauzy/contracts'; import { environment } from '@gauzy/ui-config'; -import { API_PREFIX } from '@gauzy/ui-core/common'; -import { Store } from '@gauzy/ui-core/common'; +import { API_PREFIX, distinctUntilChange, Store } from '@gauzy/ui-core/common'; @UntilDestroy({ checkProperties: true }) @Component({ template: '' }) export class ImageUploaderBaseComponent { - user: IUser; - uploader: FileUploader; + public organization: IOrganization; + public user: IUser; + public uploader: FileUploader; protected subject$: Subject = new Subject(); /* @@ -34,6 +34,14 @@ export class ImageUploaderBaseComponent { } onInit() { + this.store.selectedOrganization$ + .pipe( + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => (this.organization = organization)), + untilDestroyed(this) + ) + .subscribe(); this.store.user$ .pipe( filter((user: IUser) => !!user), @@ -60,18 +68,24 @@ export class ImageUploaderBaseComponent { const uploaderOptions: FileUploaderOptions = { url: environment.API_BASE_URL + `${API_PREFIX}/image-assets/upload/${this.folder}`, - // XHR request method - method: 'POST', - // Upload files automatically upon addition to upload queue - autoUpload: true, - // Use xhrTransport in favor of iframeTransport - isHTML5: true, - // Calculate progress independently for each uploaded file - removeAfterUpload: true, - // XHR request headers - headers: headers + method: 'POST', // XHR request method + autoUpload: true, // Upload files automatically upon addition to upload queue + isHTML5: true, // Use xhrTransport in favor of iframeTransport + removeAfterUpload: true, // Calculate progress independently for each uploaded file + headers: headers // XHR request headers }; this.uploader = new FileUploader(uploaderOptions); + + // Adding additional form data + this.uploader.onBuildItemForm = (fileItem: FileItem, form) => { + if (!!this.store.user.tenantId) { + form.append('tenantId', tenantId); + } + + if (!!this.organization) { + form.append('organizationId', this.organization.id); + } + }; } /** diff --git a/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader.component.ts b/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader.component.ts index b127815a6e0..d386f8ab2b5 100644 --- a/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader.component.ts +++ b/packages/ui-core/src/lib/shared/src/image-uploader/image-uploader.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, Input, Output, EventEmitter, AfterViewInit } from '@angular/core'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { FileUploader, FileUploaderOptions } from 'ng2-file-upload'; +import { FileItem, FileUploader, FileUploaderOptions } from 'ng2-file-upload'; import { filter, tap } from 'rxjs/operators'; -import { IImageAsset, IUser } from '@gauzy/contracts'; -import { API_PREFIX, Store } from '@gauzy/ui-core/common'; +import { IImageAsset, IOrganization, IUser } from '@gauzy/contracts'; +import { API_PREFIX, distinctUntilChange, Store } from '@gauzy/ui-core/common'; import { environment } from '@gauzy/ui-config'; @UntilDestroy() @@ -24,12 +24,13 @@ import { environment } from '@gauzy/ui-config'; styleUrls: ['./image-uploader.component.scss'] }) export class ImageUploaderComponent implements AfterViewInit, OnInit { - user: IUser; - uploader: FileUploader; + public organization: IOrganization; + public user: IUser; + public uploader: FileUploader; /* * Getter & Setter for dynamic file uploader style element */ - _styles: Object = { + private _styles: Object = { width: '100%', opacity: '0', position: 'absolute', @@ -46,7 +47,7 @@ export class ImageUploaderComponent implements AfterViewInit, OnInit { /* * Getter & Setter for dynamic image upload folder */ - _folder: string = 'profile_pictures'; + private _folder: string = 'profile_pictures'; get folder(): string { return this._folder; } @@ -61,6 +62,14 @@ export class ImageUploaderComponent implements AfterViewInit, OnInit { constructor(private readonly store: Store) {} ngOnInit() { + this.store.selectedOrganization$ + .pipe( + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => (this.organization = organization)), + untilDestroyed(this) + ) + .subscribe(); this.store.user$ .pipe( filter((user: IUser) => !!user), @@ -72,6 +81,9 @@ export class ImageUploaderComponent implements AfterViewInit, OnInit { } ngAfterViewInit() { + this.uploader.onAfterAddingFile = (file) => { + file.withCredentials = false; + }; this.uploader.onSuccessItem = (item: any, response: string, status: number) => { try { if (response) { @@ -103,30 +115,45 @@ export class ImageUploaderComponent implements AfterViewInit, OnInit { } } + /** + * Load settings for the file uploader, including headers and additional form data. + * + * @returns void + */ private _loadUploaderSettings() { if (!this.user) { return; } - const { token } = this.store; - const { tenantId } = this.user; + const token = this.store.token; + const tenantId = this.user.tenantId; const headers: Array<{ name: string; value: string }> = []; headers.push({ name: 'Authorization', value: `Bearer ${token}` }); headers.push({ name: 'Tenant-Id', value: tenantId }); + if (!!this.organization) { + headers.push({ name: 'Organization-Id', value: `${this.organization.id}` }); + } + const uploaderOptions: FileUploaderOptions = { url: environment.API_BASE_URL + `${API_PREFIX}/image-assets/upload/${this.folder}`, - // XHR request method - method: 'POST', - // Upload files automatically upon addition to upload queue - autoUpload: true, - // Use xhrTransport in favor of iframeTransport - isHTML5: true, - // Calculate progress independently for each uploaded file - removeAfterUpload: true, - // XHR request headers - headers: headers + method: 'POST', // XHR request method + autoUpload: true, // Upload files automatically upon addition to upload queue + isHTML5: true, // Use xhrTransport in favor of iframeTransport + removeAfterUpload: true, // Calculate progress independently for each uploaded file + headers: headers // XHR request headers }; this.uploader = new FileUploader(uploaderOptions); + + // Adding additional form data + this.uploader.onBuildItemForm = (fileItem: FileItem, form) => { + if (!!this.store.user.tenantId) { + form.append('tenantId', tenantId); + } + + if (!!this.organization) { + form.append('organizationId', this.organization.id); + } + }; } } diff --git a/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.html b/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.html index 2547bb784d1..c569e23a858 100644 --- a/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.html +++ b/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.html @@ -1,37 +1,37 @@
- - - - - - - - {{ item.full_name }} + + + + - - - {{ item.full_name }} + + + {{ item.full_name }} - + + + {{ item.full_name }} + +
diff --git a/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.ts b/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.ts index b1f26429859..8561442c3b5 100644 --- a/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.ts +++ b/packages/ui-core/src/lib/shared/src/integrations/github/repository-selector/repository-selector.component.ts @@ -1,16 +1,10 @@ -import { Component, OnInit, forwardRef, OnDestroy, AfterViewInit, Input, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, forwardRef, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject, of } from 'rxjs'; import { catchError, finalize, map, tap } from 'rxjs/operators'; import { Observable } from 'rxjs/internal/Observable'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { - HttpStatus, - IGithubRepository, - IGithubRepositoryResponse, - IIntegrationTenant, - IOrganization -} from '@gauzy/contracts'; +import { IGithubRepository, IGithubRepositoryResponse, IIntegrationTenant, IOrganization } from '@gauzy/contracts'; import { ErrorHandlingService, GithubService } from '@gauzy/ui-core/core'; import { Store } from '@gauzy/ui-core/common'; @@ -27,7 +21,7 @@ import { Store } from '@gauzy/ui-core/common'; } ] }) -export class RepositorySelectorComponent implements AfterViewInit, OnInit, OnDestroy { +export class RepositorySelectorComponent implements OnInit, OnDestroy { public preSelected: boolean = false; public loading: boolean = false; private subject$: Subject = new Subject(); @@ -110,15 +104,19 @@ export class RepositorySelectorComponent implements AfterViewInit, OnInit, OnDes ngOnInit(): void {} - ngAfterViewInit(): void {} - /** + * Pre-selects a repository based on the provided source ID. * - * @param sourceId + * @param sourceId - The ID of the source repository to pre-select. */ private _preSelectedRepository(sourceId: IGithubRepository['id']) { + // Find the repository in the list of repositories using the source ID const repository = this.repositories.find((repository: IGithubRepository) => repository.id === sourceId); - this.selectRepository(repository); + + // If the repository is found, select it + if (repository) { + this.selectRepository(repository); + } } /** @@ -129,40 +127,35 @@ export class RepositorySelectorComponent implements AfterViewInit, OnInit, OnDes if (!this.organization) { return; } + this.loading = true; // Extract organization properties const { id: organizationId, tenantId } = this.organization; const { id: integrationId } = this.integration; - this.repositories$ = this._githubService - .getRepositories(integrationId, { - organizationId, - tenantId - }) - .pipe( - tap((response: IGithubRepositoryResponse) => { - if (response['status'] == HttpStatus.INTERNAL_SERVER_ERROR) { - throw new Error(`${response['message']}`); - } - }), - map(({ repositories }: IGithubRepositoryResponse) => repositories), - // Update component state with fetched repositories - tap((repositories: IGithubRepository[]) => { - this.repositories = repositories; - this.afterLoad.emit(this.repositories || []); - }), - catchError((error) => { - // Handle and log errors - this._errorHandlingService.handleError(error); - return of([]); - }), - finalize(() => { - this.loading = false; - }), - // Handle component lifecycle to avoid memory leaks - untilDestroyed(this) - ); + const repositories$ = this._githubService.getRepositories(integrationId, { + organizationId, + tenantId + }); + this.repositories$ = repositories$.pipe( + map(({ repositories }: IGithubRepositoryResponse) => repositories), + // Update component state with fetched repositories + tap((repositories: IGithubRepository[]) => { + this.repositories = repositories; + this.afterLoad.emit(this.repositories || []); + }), + catchError((error) => { + // Handle and log errors + this._errorHandlingService.handleError(error); + return of([]); + }), + finalize(() => { + this.loading = false; + }), + // Handle component lifecycle to avoid memory leaks + untilDestroyed(this) + ); } /** diff --git a/packages/ui-core/src/lib/shared/src/legal/privacy-policy/privacy-policy.component.scss b/packages/ui-core/src/lib/shared/src/legal/privacy-policy/privacy-policy.component.scss index dd4176d1cdd..b48aaf87323 100644 --- a/packages/ui-core/src/lib/shared/src/legal/privacy-policy/privacy-policy.component.scss +++ b/packages/ui-core/src/lib/shared/src/legal/privacy-policy/privacy-policy.component.scss @@ -32,7 +32,6 @@ } ::ng-deep { - h1 { color: var(--gauzy-text-color-1) !important; font-size: 24px !important; @@ -83,7 +82,7 @@ line-height: 19px; } - .iubenda_policy .simple_pp>ul>li>ul .iconed { + .iubenda_policy .simple_pp > ul > li > ul .iconed { padding-left: 40px !important; background-repeat: no-repeat; background-color: transparent; @@ -101,12 +100,12 @@ font-size: 13px; font-weight: normal; line-height: 18px; - margin-bottom: 9px + margin-bottom: 9px; } .iubenda_policy p small { font-size: 11px; - color: #bfbfbf + color: #bfbfbf; } .iubenda_policy h1, @@ -116,71 +115,71 @@ h5, h6 { font-weight: bold; - color: #59636D + color: #59636d; } .iubenda_policy h1 { margin-bottom: 18px; font-size: 30px; - line-height: 2 + line-height: 2; } .iubenda_policy h1 small { - font-size: 18px + font-size: 18px; } .iubenda_policy h2 { font-size: 24px; margin-bottom: 18px; - line-height: 1.5 + line-height: 1.5; } .iubenda_policy h2 small { - font-size: 14px + font-size: 14px; } .iubenda_policy h3, h4, h5, h6 { - margin-bottom: 9px + margin-bottom: 9px; } .iubenda_policy h3 { - font-size: 18px + font-size: 18px; } .iubenda_policy h3 small { - font-size: 14px + font-size: 14px; } .iubenda_policy h4 { - font-size: 16px + font-size: 16px; } .iubenda_policy h4 small { font-weight: bold; - font-size: 13px + font-size: 13px; } .iubenda_policy h5 { font-size: 13px; - padding-top: 19px + padding-top: 19px; } .iubenda_policy h6 { font-size: 13px; color: #bfbfbf; - text-transform: uppercase + text-transform: uppercase; } .iubenda_policy ul ul { - margin: 0 + margin: 0; } .iubenda_policy ul.styled { list-style: disc; - padding-top: 5px + padding-top: 5px; } .iubenda_policy ul.styled li { @@ -188,54 +187,54 @@ line-height: 19px; font-size: 13px; margin-left: 30px; - margin-top: 2px + margin-top: 2px; } .iubenda_policy ol { - list-style: decimal + list-style: decimal; } .iubenda_policy ul.unstyled { list-style: none; - margin-left: 0 + margin-left: 0; } .iubenda_policy dl { - margin-bottom: 18px + margin-bottom: 18px; } .iubenda_policy dl dt, dl dd { - line-height: 18px + line-height: 18px; } .iubenda_policy dl dt { - font-weight: bold + font-weight: bold; } .iubenda_policy dl dd { - margin-left: 9px + margin-left: 9px; } .iubenda_policy hr { margin: 0 0 19px; border: 0; - border-bottom: 1px solid #eee + border-bottom: 1px solid #eee; } .iubenda_policy strong { font-style: inherit; - font-weight: bold + font-weight: bold; } .iubenda_policy em { font-style: italic; font-weight: inherit; - line-height: inherit + line-height: inherit; } .iubenda_policy .muted { - color: #bfbfbf + color: #bfbfbf; } .iubenda_policy blockquote { @@ -248,7 +247,7 @@ font-size: 14px; font-weight: 300; line-height: 18px; - margin-bottom: 0 + margin-bottom: 0; } .iubenda_policy blockquote small { @@ -256,17 +255,17 @@ font-size: 12px; font-weight: 300; line-height: 18px; - color: #bfbfbf + color: #bfbfbf; } .iubenda_policy blockquote small:before { - content: '\2014 \00A0' + content: '\2014 \00A0'; } .iubenda_policy address { display: block; line-height: 18px; - margin-bottom: 18px + margin-bottom: 18px; } .iubenda_policy code, @@ -274,13 +273,13 @@ padding: 0 3px 2px; font-family: Monaco, Andale Mono, Courier New, monospace; font-size: 12px; - border-radius: 3px + border-radius: 3px; } .iubenda_policy code { background-color: #fee9cc; color: rgba(0, 0, 0, 0.75); - padding: 1px 3px + padding: 1px 3px; } .iubenda_policy pre { @@ -295,49 +294,49 @@ border-radius: 3px; white-space: pre; white-space: pre-wrap; - word-wrap: break-word + word-wrap: break-word; } .iubenda_policy .breadcrumbs { padding: 0 0 10px 0; margin-bottom: 30px; - border-bottom: 1px solid #F6F6F6; - width: 100% + border-bottom: 1px solid #f6f6f6; + width: 100%; } - .iubenda_policy .breadcrumbs>li { + .iubenda_policy .breadcrumbs > li { float: left; filter: alpha(opacity=50); -khtml-opacity: 0.5; -moz-opacity: 0.5; - opacity: 0.5 + opacity: 0.5; } - .iubenda_policy .breadcrumbs>li:not(:last-child):after { - color: #333B43; + .iubenda_policy .breadcrumbs > li:not(:last-child):after { + color: #333b43; padding: 0 10px; - content: "\203a" + content: '\203a'; } - .iubenda_policy .breadcrumbs+.pills, - .breadcrumbs+.sec_tabs { - margin-top: -15px + .iubenda_policy .breadcrumbs + .pills, + .breadcrumbs + .sec_tabs { + margin-top: -15px; } .iubenda_policy .cust_row { display: table-row; - margin: 0 + margin: 0; } .iubenda_policy .column { display: table-cell; vertical-align: top; - padding: 30px + padding: 30px; } .box_primary { - -webkit-box-shadow: var(--gauzy-shadow)(0 0 0 / 15%) !important; - box-shadow: var(--gauzy-shadow)(0 0 0 / 15%) !important; + -webkit-box-shadow: var(--gauzy-shadow) (0 0 0 / 15%) !important; + box-shadow: var(--gauzy-shadow) (0 0 0 / 15%) !important; border: unset !important; border-radius: var(--border-radius) !important; background: var(--gauzy-card-4) !important; @@ -345,91 +344,91 @@ .box_content { border-radius: 4px; - padding: 30px + padding: 30px; } .box_content .iub_content { - padding: 30px + padding: 30px; } - .box_content .iub_content>hr { + .box_content .iub_content > hr { width: 686px; margin-left: -30px; - margin-right: -30px + margin-right: -30px; } .box_content .aside { width: 191px; - padding: 30px + padding: 30px; } .box_content .aside.aside-right { - border-left: 1px solid #DFDFDF + border-left: 1px solid #dfdfdf; } - .table>.box_content { - padding: 0 + .table > .box_content { + padding: 0; } .box_10 { padding: 10px; border-radius: 3px; - margin-bottom: 15px + margin-bottom: 15px; } - .box_10>h4 { + .box_10 > h4 { margin-bottom: 0; - font-size: 13px + font-size: 13px; } - .box_10>.w_icon, - .box_10.expand>.w_icon, - .box_10>.w_icon.expand-click, - .box_10.expand>.w_icon.expand-click { + .box_10 > .w_icon, + .box_10.expand > .w_icon, + .box_10 > .w_icon.expand-click, + .box_10.expand > .w_icon.expand-click { padding-left: 45px !important; background-repeat: no-repeat; background-color: transparent; background-position-x: 10px; background-position-y: 7px; - background-position: 10px 7px + background-position: 10px 7px; } - .box_10>.w_icon_16, - .box_10.expand>.w_icon_16, - .box_10>.w_icon_16.expand-click, - .box_10.expand>.w_icon_16.expand-click { + .box_10 > .w_icon_16, + .box_10.expand > .w_icon_16, + .box_10 > .w_icon_16.expand-click, + .box_10.expand > .w_icon_16.expand-click { padding-left: 40px !important; background-repeat: no-repeat; background-color: transparent; background-position-x: 11px; background-position-y: 11px; - background-position: 11px 11px + background-position: 11px 11px; } - .box_10>.w_icon_24, - .box_10.expand>.w_icon_24, - .box_10>.w_icon_24.expand-click, - .box_10.expand>.w_icon_24.expand-click { + .box_10 > .w_icon_24, + .box_10.expand > .w_icon_24, + .box_10 > .w_icon_24.expand-click, + .box_10.expand > .w_icon_24.expand-click { padding-left: 45px !important; background-repeat: no-repeat; background-color: transparent; background-position-x: 10px; background-position-y: 10px; - background-position: 10px 10px + background-position: 10px 10px; } .box_5 { padding: 5px; border-radius: 3px; font-size: 11px; - margin-bottom: 15px + margin-bottom: 15px; } .box_5 hr { padding-top: 5px; margin: 0 -5px 5px -5px; border: 0; - border-bottom: 1px solid #AC3737 + border-bottom: 1px solid #ac3737; } .box_5.w_icon_16 { @@ -437,7 +436,7 @@ background-repeat: no-repeat; background-position-x: 8px; background-position-y: 6px; - background-position: 8px 6px + background-position: 8px 6px; } .box_5.w_icon_16 hr { @@ -445,181 +444,181 @@ padding-left: 30px !important; padding-right: 5px; margin-left: -30px; - margin-right: -5px + margin-right: -5px; } .box_5.w_icon_16.red { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAATlBMVEUAAAD%2F%2F%2F8AAAD%2F%2F%2F8AAAAAAAD%2F%2F%2F%2F%2F%2F%2F%2FT09P%2F%2F%2F%2F9%2Ff3Y2Nj9%2Ff39%2Ff3d3d3%2F%2F%2F%2F8%2FPz39%2Ff19fX%2B%2Fv79%2Ff34%2BPj5%2Bfn8%2FPz9%2Ff3%2F%2F%2F8ZO4GEAAAAGXRSTlMAEB0gMDNAUHSAgYSRoaWwsra3weLl5fLyUJhrdwAAAF1JREFUeF6NzUcWhCAAwFAQsIPOWCD3v6gPxLYjy7%2BJKE1Ok%2FxAD%2BMbFIB6wYIxLA%2FUbEJAc8PKHmG9oAOkArq8DICdgXCuLUA7EDkBsd%2BfWALnyXmXoNImpytR0AEwdQcUE5t8VQAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAATlBMVEUAAAD%2F%2F%2F8AAAD%2F%2F%2F8AAAAAAAD%2F%2F%2F%2F%2F%2F%2F%2FT09P%2F%2F%2F%2F9%2Ff3Y2Nj9%2Ff39%2Ff3d3d3%2F%2F%2F%2F8%2FPz39%2Ff19fX%2B%2Fv79%2Ff34%2BPj5%2Bfn8%2FPz9%2Ff3%2F%2F%2F8ZO4GEAAAAGXRSTlMAEB0gMDNAUHSAgYSRoaWwsra3weLl5fLyUJhrdwAAAF1JREFUeF6NzUcWhCAAwFAQsIPOWCD3v6gPxLYjy7%2BJKE1Ok%2FxAD%2BMbFIB6wYIxLA%2FUbEJAc8PKHmG9oAOkArq8DICdgXCuLUA7EDkBsd%2BfWALnyXmXoNImpytR0AEwdQcUE5t8VQAAAABJRU5ErkJggg%3D%3D); } .box_thumb { - background: #FFF; + background: #fff; -webkit-box-shadow: 0 0 1px #a3a3a3, 0 1px 1px #a3a3a3; box-shadow: 0 0 1px #a3a3a3, 0 1px 1px #a3a3a3; - padding: 6px + padding: 6px; } footer { margin-top: 17px; padding-top: 17px; - border-top: 1px solid #eee + border-top: 1px solid #eee; } hr { padding-top: 15px; - margin: 0 0 15px 0 + margin: 0 0 15px 0; } hr.primary { border: 0; - border-bottom: 1px solid #DFDFDF; + border-bottom: 1px solid #dfdfdf; -webkit-box-shadow: 0 1px 0 #f7f7f7; - box-shadow: 0 1px 0 #f7f7f7 + box-shadow: 0 1px 0 #f7f7f7; } .blue { color: #fff; - background-color: #0073CE + background-color: #0073ce; } .yellow { - color: #6D693D; - background-color: #FFD24D + color: #6d693d; + background-color: #ffd24d; } .red { - color: #FFF; - background-color: #FF5D4D + color: #fff; + background-color: #ff5d4d; } .red a, .red a:hover:not(.btn) { - color: #FFF + color: #fff; } .red a { - border-bottom-color: rgba(247, 247, 247, 0.3) + border-bottom-color: rgba(247, 247, 247, 0.3); } .red a:hover { - border-bottom-color: rgba(247, 247, 247, 0.6) + border-bottom-color: rgba(247, 247, 247, 0.6); } .green { - color: #4D6C47; - background-color: #F1FFD5 + color: #4d6c47; + background-color: #f1ffd5; } .iubgreen { color: #ffffff; - background-color: #1CC691 + background-color: #1cc691; } .azure { color: #364048; - background-color: #D2ECFE + background-color: #d2ecfe; } .white { - color: #54616B; - background-color: #F8F8F8 + color: #54616b; + background-color: #f8f8f8; } .black { - color: #FFF; - background-color: #333333 + color: #fff; + background-color: #333333; } .trasp { - color: #333B43; - background-color: #FFFFFF + color: #333b43; + background-color: #ffffff; } .fade { -webkit-transition: opacity 0.15s linear; transition: opacity 0.15s linear; - opacity: 0 + opacity: 0; } .fade.in { - opacity: 1 + opacity: 1; } .expand-click { cursor: pointer; - position: relative + position: relative; } .box_10.expand .expand-click { margin: -10px; - padding: 12px 25px 13px 10px + padding: 12px 25px 13px 10px; } .box_10.expand .expand-content { - margin-top: 10px + margin-top: 10px; } - .box_10.expand .expand-content>*:first-child { + .box_10.expand .expand-content > *:first-child { margin-top: 0; - padding-top: 0 + padding-top: 0; } .expand.expanded .expand-click:after, .box_10.expand.expanded .expand-click:after { - content: ""; + content: ''; position: absolute; right: 10px; top: 19px; border: 5px; border-color: transparent; border-style: solid; - border-top-color: #333B43 + border-top-color: #333b43; } .expand .expand-click, .box_10.expand .expand-click, .expand.expanded .expand-click, .box_10.expand.expanded .expand-click { - border-bottom: 1px dotted #DDD; + border-bottom: 1px dotted #ddd; margin-bottom: 10px; -webkit-transition: 0.2s linear all; - transition: 0.2s linear all + transition: 0.2s linear all; } .expand.collapsed .expand-click { border-bottom: 0; - margin-bottom: -10px + margin-bottom: -10px; } .expand.collapsed .expand-click:after { - content: ""; + content: ''; position: absolute; right: 10px; top: 17px; border: 5px; border-color: transparent; border-style: solid; - border-right-color: #333B43 + border-right-color: #333b43; } .all-collapsed .expand .expand-click:after { - content: ""; + content: ''; position: absolute; right: 10px; top: 17px; border: 5px; border-color: transparent; border-style: solid; - border-right-color: #333B43 + border-right-color: #333b43; } .all-collapsed .expand .expand-click { border-bottom: 0; - margin-bottom: -10px + margin-bottom: -10px; } .all-collapsed .expand-content { - display: none + display: none; } .iub_container-fluid { @@ -627,163 +626,165 @@ min-width: 940px; padding-left: 20px !important; padding-right: 20px; - zoom: 1 + zoom: 1; } .iub_container-fluid:before, .iub_container-fluid:after { display: table; - content: ""; + content: ''; zoom: 1; - *display: inline + display: inline; } .iub_container-fluid:after { - clear: both + clear: both; } - .iub_container-fluid>.sidebar { + .iub_container-fluid > .sidebar { float: left; - width: 220px + width: 220px; } - .iub_container-fluid>.iub_content { - margin-left: 240px + .iub_container-fluid > .iub_content { + margin-left: 240px; } .iubenda_policy a { text-decoration: none; font-weight: bold; - border-bottom: 1px solid #F6F6F6; - color: #333b43 + border-bottom: 1px solid #f6f6f6; + color: #333b43; } .iubenda_policy a.unstyled { - border-bottom: 0 + border-bottom: 0; } .iubenda_policy a:hover:not(.btn) { color: #121518; - border-bottom-color: #D6D6D6; + border-bottom-color: #d6d6d6; -webkit-transition: 0.1s linear all; - transition: 0.1s linear all + transition: 0.1s linear all; } .iubenda_policy a:focus { - outline: none + outline: none; } .iubenda_policy a.no_border, a.no_border:hover { - border-bottom-width: 0 + border-bottom-width: 0; } .iubenda_policy .pull-right { - float: right + float: right; } .pull-left { - float: left + float: left; } .hide { - display: none + display: none; } .show { - display: block + display: block; } .link_on_dark a { - border-bottom-color: rgba(247, 247, 247, 0.3) + border-bottom-color: rgba(247, 247, 247, 0.3); } .link_on_dark a:hover { - border-bottom-color: rgba(247, 247, 247, 0.6) + border-bottom-color: rgba(247, 247, 247, 0.6); } - [class*="policyicon_"] { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAGFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz9%2BjSc3AAAAB3RSTlMAEEBQgMzQxeXuPgAAADJJREFUGFdjYMAJWEPhIAAPh70cDgoGK6cI5B8Yp6S8TACJk4gkA5RAcBKR9BQLoAUOAATNYYOCulUNAAAAAElFTkSuQmCC) + [class*='policyicon_'] { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAGFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz9%2BjSc3AAAAB3RSTlMAEEBQgMzQxeXuPgAAADJJREFUGFdjYMAJWEPhIAAPh70cDgoGK6cI5B8Yp6S8TACJk4gkA5RAcBKR9BQLoAUOAATNYYOCulUNAAAAAElFTkSuQmCC); } .policyicon_pdt_68 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAKVJREFUeF51jlmWwyAMBGXEboT6%2FqedIZAAJqnfer3QJKpGOrkKakW5noIrAlFA5V0EKL%2B8Iqw1d%2B%2FojflTx4JlNUJGnVe1tOBUfRMZYmjDCDKRINFBglCLnXiltnTClfAtEgACxvHJldHF4xYL3gLq1l1Mgfk5AZtQx%2FYfdroL4TySXFeRWTAQc0%2Fhe0FHbRiicsJGZG3iNgUPiimgYBUHlQP94g9%2BZg8xOTGEFAAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAKVJREFUeF51jlmWwyAMBGXEboT6%2FqedIZAAJqnfer3QJKpGOrkKakW5noIrAlFA5V0EKL%2B8Iqw1d%2B%2FojflTx4JlNUJGnVe1tOBUfRMZYmjDCDKRINFBglCLnXiltnTClfAtEgACxvHJldHF4xYL3gLq1l1Mgfk5AZtQx%2FYfdroL4TySXFeRWTAQc0%2Fhe0FHbRiicsJGZG3iNgUPiimgYBUHlQP94g9%2BZg8xOTGEFAAAAABJRU5ErkJggg%3D%3D); } .policyicon_purpose_5 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAElBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2BtTDCxAAAABXRSTlMAECBAgLf%2B2%2BsAAABGSURBVBhXY2AAA5ZQBwY4YA0NIJfjCjYHygkNDUTmBGPhgOyFc1iB6pE4wSAOUAGCIxoaiOCYhgYjOKqhQThkyODAAR4OAI98N9LK6tL3AAAAAElFTkSuQmCC) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAElBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2BtTDCxAAAABXRSTlMAECBAgLf%2B2%2BsAAABGSURBVBhXY2AAA5ZQBwY4YA0NIJfjCjYHygkNDUTmBGPhgOyFc1iB6pE4wSAOUAGCIxoaiOCYhgYjOKqhQThkyODAAR4OAI98N9LK6tL3AAAAAElFTkSuQmCC); } .policyicon_purpose_7 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAINJREFUeF6V0UsOxCAIBmB8tVoZ4f6nnUqaoFUW%2FVeEj0hUMOKM9kE7CBcxr93SuGcCf%2FRZniCmXGVUwZV2M78DgYRXQDaAP0OzIJIB4C%2FaQo%2BTCyK9ISFizimAPyuNACjlKXW6SMF30B9I9YFndRieuZCCHKU0QIU1LDEhrvDrQG6EP%2FDZElAL0vLHAAAAAElFTkSuQmCC) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAINJREFUeF6V0UsOxCAIBmB8tVoZ4f6nnUqaoFUW%2FVeEj0hUMOKM9kE7CBcxr93SuGcCf%2FRZniCmXGVUwZV2M78DgYRXQDaAP0OzIJIB4C%2FaQo%2BTCyK9ISFizimAPyuNACjlKXW6SMF30B9I9YFndRieuZCCHKU0QIU1LDEhrvDrQG6EP%2FDZElAL0vLHAAAAAElFTkSuQmCC); } .policyicon_purpose_9 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAACC0lEQVQ4y7XSO2gWVhQH8BPxDRG%2BEhurMcSI4GsoPqjkZ6BLwcFFHUQJKqbEwRciDqZDF90cpIsILtZHh0KTIdQMgkTRiIshBoWgTRpbsVaxgqRf4uM4JCHfRzpIwXun8%2Bf%2BuHDOifj%2FxwoD2qek7Qat%2FG9Qr1%2FblLRNv%2FqyqKHCjIgIqw3oGE9mmtlQERGhw4DVERFmNFREhG91uq6gxUspnVdlky5dNqlyXkovtSi4rtPe8JeUaq1yWLN9tkVoklJThK1a7HXISrVSehpSGrXb5woWqFZljZNSOmmtBRapUe0Lu4xKOQZSr0633dejS7chKQ25p0%2BvHn3u6Bt7OQFSeuWG3pI6DbvpZ5dc8WwimwTPbYswx49Sei89sDNCpaoI6%2FyqWA5OmxUR4StF6Z0hX5puvyH%2FOmeeudrLwXfjg1prUCo6FuGyty444W89CpYZKQU%2FmF3ywwvVthtxwpwImz1yzjSdpWBYq2nWuzbWoQgX%2FaPOAd%2Br1O55hDOl4LHdDRXqnPVWehLhlPSNgiURFlof4adJMGC7eRERarRKr32t2qBn9lhlg%2BVq7fDbJDhasp%2BfueW9brOscdULv7vntlselnZpadlKH5fSRYvN16ytdJgT4KBGGzVqtNFmv4yndzWrt8WjqSCNGFZUNOxN2Xq8K6%2FD47Et%2FKg7ajAc9edHgz8ciU9%2BPgBKt4%2FTzlslzAAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAACC0lEQVQ4y7XSO2gWVhQH8BPxDRG%2BEhurMcSI4GsoPqjkZ6BLwcFFHUQJKqbEwRciDqZDF90cpIsILtZHh0KTIdQMgkTRiIshBoWgTRpbsVaxgqRf4uM4JCHfRzpIwXun8%2Bf%2BuHDOifj%2FxwoD2qek7Qat%2FG9Qr1%2FblLRNv%2FqyqKHCjIgIqw3oGE9mmtlQERGhw4DVERFmNFREhG91uq6gxUspnVdlky5dNqlyXkovtSi4rtPe8JeUaq1yWLN9tkVoklJThK1a7HXISrVSehpSGrXb5woWqFZljZNSOmmtBRapUe0Lu4xKOQZSr0633dejS7chKQ25p0%2BvHn3u6Bt7OQFSeuWG3pI6DbvpZ5dc8WwimwTPbYswx49Sei89sDNCpaoI6%2FyqWA5OmxUR4StF6Z0hX5puvyH%2FOmeeudrLwXfjg1prUCo6FuGyty444W89CpYZKQU%2FmF3ywwvVthtxwpwImz1yzjSdpWBYq2nWuzbWoQgX%2FaPOAd%2Br1O55hDOl4LHdDRXqnPVWehLhlPSNgiURFlof4adJMGC7eRERarRKr32t2qBn9lhlg%2BVq7fDbJDhasp%2BfueW9brOscdULv7vntlselnZpadlKH5fSRYvN16ytdJgT4KBGGzVqtNFmv4yndzWrt8WjqSCNGFZUNOxN2Xq8K6%2FD47Et%2FKg7ajAc9edHgz8ciU9%2BPgBKt4%2FTzlslzAAAAABJRU5ErkJggg%3D%3D); } .policyicon_purpose_10, .policyicon_purpose_15 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAKVJREFUeF51jlmWwyAMBGXEboT6%2FqedIZAAJqnfer3QJKpGOrkKakW5noIrAlFA5V0EKL%2B8Iqw1d%2B%2FojflTx4JlNUJGnVe1tOBUfRMZYmjDCDKRINFBglCLnXiltnTClfAtEgACxvHJldHF4xYL3gLq1l1Mgfk5AZtQx%2FYfdroL4TySXFeRWTAQc0%2Fhe0FHbRiicsJGZG3iNgUPiimgYBUHlQP94g9%2BZg8xOTGEFAAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAKVJREFUeF51jlmWwyAMBGXEboT6%2FqedIZAAJqnfer3QJKpGOrkKakW5noIrAlFA5V0EKL%2B8Iqw1d%2B%2FojflTx4JlNUJGnVe1tOBUfRMZYmjDCDKRINFBglCLnXiltnTClfAtEgACxvHJldHF4xYL3gLq1l1Mgfk5AZtQx%2FYfdroL4TySXFeRWTAQc0%2Fhe0FHbRiicsJGZG3iNgUPiimgYBUHlQP94g9%2BZg8xOTGEFAAAAABJRU5ErkJggg%3D%3D); } .policyicon_purpose_13 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAJ1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz9PhkGkAAAADHRSTlMAECBAUHCQoLDA4PB7ua%2BoAAAAa0lEQVR42p3QQQ6AIAxE0aEIFdr7n1eMxIAOMfEt%2B9sF4IOkYt5YSTKO1Qd6p%2BQP6Zqrvyjd7zdiLJggO5VReajwhR%2FBnDIoDwrhQcAfkhd%2FtQO0KDqf1A0kmEZgDjk2AZPzPoJo6wFEYOsHFFISOn%2BKxfoAAAAASUVORK5CYII%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAJ1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz9PhkGkAAAADHRSTlMAECBAUHCQoLDA4PB7ua%2BoAAAAa0lEQVR42p3QQQ6AIAxE0aEIFdr7n1eMxIAOMfEt%2B9sF4IOkYt5YSTKO1Qd6p%2BQP6Zqrvyjd7zdiLJggO5VReajwhR%2FBnDIoDwrhQcAfkhd%2FtQO0KDqf1A0kmEZgDjk2AZPzPoJo6wFEYOsHFFISOn%2BKxfoAAAAASUVORK5CYII%3D); } .policyicon_purpose_14 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAMFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2Fjai5RAAAAD3RSTlMAECAwUGBwgJCgsMDQ4PASl6hyAAAAfklEQVR42pXRUQ6EMAgE0MEWW21l7n9btanJWnE3%2Bz4hhCHgq5jKooKD6FJS7OVQebIIROOphlY3dqrsLABidJgg0ZWw0bWBL%2F5vvO%2FIdGVM%2Fh0TMNMx47DwYcVJKgdV0MgwUwSXfA%2F0QY2dKW7CxutHA1lbHMFTavE9qsBvOztlFTRVyS%2BYAAAAAElFTkSuQmCC) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAMFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2Fjai5RAAAAD3RSTlMAECAwUGBwgJCgsMDQ4PASl6hyAAAAfklEQVR42pXRUQ6EMAgE0MEWW21l7n9btanJWnE3%2Bz4hhCHgq5jKooKD6FJS7OVQebIIROOphlY3dqrsLABidJgg0ZWw0bWBL%2F5vvO%2FIdGVM%2Fh0TMNMx47DwYcVJKgdV0MgwUwSXfA%2F0QY2dKW7CxutHA1lbHMFTavE9qsBvOztlFTRVyS%2BYAAAAAElFTkSuQmCC); } .policyicon_purpose_16 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAJFJREFUeF6V0NsOAyEIRVE6I4rFwvn%2Fr63N3CR10nQnPK2IUdpbpKmsorJQqOKTl2xeRhDsycMgA7QDGkmfq9cI%2FvNEhGcAO8CowAbAGTEwX1XDKvYNnJM7f78clVqfydOlgwRIG6S1TwDdQEnD3cv1iWw4f54VQ1qfUO5QDDGYVLNCmOQ5O2Ea8R2kP8FWobvefhoT%2FSVCMbAAAAAASUVORK5CYII%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAM1BMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2F10LmwAAAAEHRSTlMAECAwQFBgcICQoLDA0ODwVOCoyAAAAJFJREFUeF6V0NsOAyEIRVE6I4rFwvn%2Fr63N3CR10nQnPK2IUdpbpKmsorJQqOKTl2xeRhDsycMgA7QDGkmfq9cI%2FvNEhGcAO8CowAbAGTEwX1XDKvYNnJM7f78clVqfydOlgwRIG6S1TwDdQEnD3cv1iWw4f54VQ1qfUO5QDDGYVLNCmOQ5O2Ea8R2kP8FWobvefhoT%2FSVCMbAAAAAASUVORK5CYII%3D); } .icon_ribbon { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAW0lEQVR42u3OwQkAIQxE0XSWVrazlJpdQdGDC0pQEf7A3ELmibsPV1V9pDM%2FAQAAAAAAAAAAAAAAEAXY1%2BcUwCQnITYD6niL2ASo4z3EaoDKf8qNBQHxArgK8ALKMXCw%2Bim7vwAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAW0lEQVR42u3OwQkAIQxE0XSWVrazlJpdQdGDC0pQEf7A3ELmibsPV1V9pDM%2FAQAAAAAAAAAAAAAAEAXY1%2BcUwCQnITYD6niL2ASo4z3EaoDKf8qNBQHxArgK8ALKMXCw%2Bim7vwAAAABJRU5ErkJggg%3D%3D); } .icon_owner { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAMFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2Fjai5RAAAAD3RSTlMAECAwQGBwgJCgsMDQ4PC8YWy5AAAAiElEQVR42o2QwRKFIAhFIcwyRP7%2Fb3uNlkBv0dkw3jODd4AbPHhNC7xAafqjYBRZOzUa0cHmc9IbiZsefIFtiuQ68RS7FUkNnwTWmRewLE9ewSPh73dfCgJbzxkiRxcrDGJhWVxa5MqYr1HzcLSPRo2ojcoZAcyV2F1MzaPoxIqcP4gGkP5BcAIxQBCQ7o5t3AAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAMFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz%2Fjai5RAAAAD3RSTlMAECAwQGBwgJCgsMDQ4PC8YWy5AAAAiElEQVR42o2QwRKFIAhFIcwyRP7%2Fb3uNlkBv0dkw3jODd4AbPHhNC7xAafqjYBRZOzUa0cHmc9IbiZsefIFtiuQ68RS7FUkNnwTWmRewLE9ewSPh73dfCgJbzxkiRxcrDGJhWVxa5MqYr1HzcLSPRo2ojcoZAcyV2F1MzaPoxIqcP4gGkP5BcAIxQBCQ7o5t3AAAAABJRU5ErkJggg%3D%3D); } .icon_general { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAGFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz9%2BjSc3AAAAB3RSTlMAEEBQgMzQxeXuPgAAADJJREFUGFdjYMAJWEPhIAAPh70cDgoGK6cI5B8Yp6S8TACJk4gkA5RAcBKR9BQLoAUOAATNYYOCulUNAAAAAElFTkSuQmCC) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAGFBMVEUAAAA%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz8%2FPz9%2BjSc3AAAAB3RSTlMAEEBQgMzQxeXuPgAAADJJREFUGFdjYMAJWEPhIAAPh70cDgoGK6cI5B8Yp6S8TACJk4gkA5RAcBKR9BQLoAUOAATNYYOCulUNAAAAAElFTkSuQmCC); } .icon_temple_24 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABwklEQVR42s3Wu0vDUBjGYS%2BLsbZSSaWgguAFHFztYEmFbp0E%2FwOrgotQFyetOKiTLqKCWCenbl20S8FFERydBLt1KV7QwUGR4094hQymjYpi4SGH9zvf%2BUgCoQ3GmF%2F1dwNq%2FRzHaUwkEn24lP73rEaL%2FwEcZmEcJexiDyfKrG8P0OG9OIDBrCvPKMuh98sDaApiAmWYj8fiqg%2FjSrWy9gbrDlDzEHIwLi9YRieiWMOrakIPvZ4DKHYhjTsYD%2Be48Kqrdwpdnw1I4RAFbCKHHWxhX%2BtjHGFb2ZbynHoKOiNV7x3YrnWLrmFFWqvm6vH7DmK4ho0l5NGGCialoiyvPbZ6Yn4GOHhCFBsoIQSDOTHKStoTVY%2FjZ0Act7CxiiICqGJaqsqK2mOrJ%2F6VARGs6ZA2ZTNyq6yoPZF%2FNWC0xiOaEq9HNOpnQBIG3djFGcLKFsQoO8UOepQl%2FQyIIKtP9BjSylcwghhWlKWR0N4sIp4D1NCKDgR0DSIEGxbCYikLibvH8voWNWMdD6jiEXe4waOye1GdGntc9Qcso8nrDtoxgEFdIcp81INed7CIBWQw%2F00ZnbH42YAXef4RnfNn%2FyreALybXwSLU3v7AAAAAElFTkSuQmCC) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABwklEQVR42s3Wu0vDUBjGYS%2BLsbZSSaWgguAFHFztYEmFbp0E%2FwOrgotQFyetOKiTLqKCWCenbl20S8FFERydBLt1KV7QwUGR4094hQymjYpi4SGH9zvf%2BUgCoQ3GmF%2F1dwNq%2FRzHaUwkEn24lP73rEaL%2FwEcZmEcJexiDyfKrG8P0OG9OIDBrCvPKMuh98sDaApiAmWYj8fiqg%2FjSrWy9gbrDlDzEHIwLi9YRieiWMOrakIPvZ4DKHYhjTsYD%2Be48Kqrdwpdnw1I4RAFbCKHHWxhX%2BtjHGFb2ZbynHoKOiNV7x3YrnWLrmFFWqvm6vH7DmK4ho0l5NGGCialoiyvPbZ6Yn4GOHhCFBsoIQSDOTHKStoTVY%2FjZ0Act7CxiiICqGJaqsqK2mOrJ%2F6VARGs6ZA2ZTNyq6yoPZF%2FNWC0xiOaEq9HNOpnQBIG3djFGcLKFsQoO8UOepQl%2FQyIIKtP9BjSylcwghhWlKWR0N4sIp4D1NCKDgR0DSIEGxbCYikLibvH8voWNWMdD6jiEXe4waOye1GdGntc9Qcso8nrDtoxgEFdIcp81INed7CIBWQw%2F00ZnbH42YAXef4RnfNn%2FyreALybXwSLU3v7AAAAAElFTkSuQmCC); } .icon_box_24 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABv0lEQVR42t3Vz0uTcRzAcUMyWhmWdAgqkKDMiBHt0A%2FYsploddwpO1gk0l%2BQEEQU0iUPgd0LIpCOCh2KkG4GTaI6mqAmWhG0CQ5jfXsdntN4bJPRxS%2B84IFnz%2Ff97Nl4Pg0hhP9qkwT%2BtTKZTCN7uM0qeTrZuc4ltQVs0EqK%2B3xmigc8Z5E39HOI7TUFfHAbJxngNQtM0kdzg5VOp7c4TvKYeeZ4wmXaYgNONHGcW7zjA6Mkqzy%2BZq4zzgxj5DhcGUgxwzSZ9TasEtvFCL%2F4WBm4SKDAPXrZV%2BPGWznFVSYIhMpAliILhEieO5ynMWbjDgZ4xjxlAn%2FiAt0skeMG44TIT8boo51zDPOeEqFSXKAnCrRHd5fgNMN8IrDCFLOUCVB74CsdZBlkL03s5xG%2FCcAGA73M0cIViswyQisn%2BFZPoIdljkWP6AIvKDFIiuV6Al2s8JIciSh0hqNk%2BVHvNwiRNd5ylwNRqKvewNnYv1z0O%2FCQ7xsIFCsDCS4xyhKhwiprhCqmGaIz%2Fm0qxBGu8YoCoYpFntLNwbS3ba3zYDdJhshTohwpMEE%2FbeyoMg%2BqTrQWbvKFSSxDJn5tsqH%2FF0QW2NxzTlYFAAAAAElFTkSuQmCC) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABv0lEQVR42t3Vz0uTcRzAcUMyWhmWdAgqkKDMiBHt0A%2FYsploddwpO1gk0l%2BQEEQU0iUPgd0LIpCOCh2KkG4GTaI6mqAmWhG0CQ5jfXsdntN4bJPRxS%2B84IFnz%2Ff97Nl4Pg0hhP9qkwT%2BtTKZTCN7uM0qeTrZuc4ltQVs0EqK%2B3xmigc8Z5E39HOI7TUFfHAbJxngNQtM0kdzg5VOp7c4TvKYeeZ4wmXaYgNONHGcW7zjA6Mkqzy%2BZq4zzgxj5DhcGUgxwzSZ9TasEtvFCL%2F4WBm4SKDAPXrZV%2BPGWznFVSYIhMpAliILhEieO5ynMWbjDgZ4xjxlAn%2FiAt0skeMG44TIT8boo51zDPOeEqFSXKAnCrRHd5fgNMN8IrDCFLOUCVB74CsdZBlkL03s5xG%2FCcAGA73M0cIViswyQisn%2BFZPoIdljkWP6AIvKDFIiuV6Al2s8JIciSh0hqNk%2BVHvNwiRNd5ylwNRqKvewNnYv1z0O%2FCQ7xsIFCsDCS4xyhKhwiprhCqmGaIz%2Fm0qxBGu8YoCoYpFntLNwbS3ba3zYDdJhshTohwpMEE%2FbeyoMg%2BqTrQWbvKFSSxDJn5tsqH%2FF0QW2NxzTlYFAAAAAElFTkSuQmCC); } .icon_tools_24 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABdklEQVR42rXVzyuDcRzA8WU3ctOag0RiO5mEg8MKcaJc%2FAlcdxppKPlZdpeTE0dqxZWaaEnh7DShlYuJbG0f78Nz%2BPj2zfM89Wz1qmff5%2FPdu%2B27tpCINNTfJzySyeQGhkPGg7UljBtrEbxDMOkaYCiKJ8QtgR0cGWt9EMe8l8AIBAOWwCGKxtqMCux5CSRQxyW61QtNo4yCEVhWgRzCboFm5CF4xTVuUYUgZQTOVeAZHV4OeRSfEMMFWoxAVt2%2FQtQ14Gzsx43anEWr5Vx61Eza9Qz%2B%2BXw71S09M6hm1v0GVtXmWCMCa2pz3BpgXc1kAg2w1oW0mjnGRJCBOYjhLshAO96MQCroM3hQMzUMBfkOZvEDUU78BFbUxl5LYBdiqPgJbKmNCUtg2xL48PpTMYVvFFDEPdqCDDwi51xHIVi0%2FAGJoew18IJ957oJFWx6CHx5DSygjgPkUULM8i2qGoFTP4ecgaCEMcshh5FFDYIzRGyBhvoF3n%2ByMxzF1ykAAAAASUVORK5CYII%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABdklEQVR42rXVzyuDcRzA8WU3ctOag0RiO5mEg8MKcaJc%2FAlcdxppKPlZdpeTE0dqxZWaaEnh7DShlYuJbG0f78Nz%2BPj2zfM89Wz1qmff5%2FPdu%2B27tpCINNTfJzySyeQGhkPGg7UljBtrEbxDMOkaYCiKJ8QtgR0cGWt9EMe8l8AIBAOWwCGKxtqMCux5CSRQxyW61QtNo4yCEVhWgRzCboFm5CF4xTVuUYUgZQTOVeAZHV4OeRSfEMMFWoxAVt2%2FQtQ14Gzsx43anEWr5Vx61Eza9Qz%2B%2BXw71S09M6hm1v0GVtXmWCMCa2pz3BpgXc1kAg2w1oW0mjnGRJCBOYjhLshAO96MQCroM3hQMzUMBfkOZvEDUU78BFbUxl5LYBdiqPgJbKmNCUtg2xL48PpTMYVvFFDEPdqCDDwi51xHIVi0%2FAGJoew18IJ957oJFWx6CHx5DSygjgPkUULM8i2qGoFTP4ecgaCEMcshh5FFDYIzRGyBhvoF3n%2ByMxzF1ykAAAAASUVORK5CYII%3D); } .icon_paper_24 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABYklEQVR42t3VP0vDQBjH8Tq4SKXiWFwcRHARVyspgq9AOjkoLoKTcymk%2BjKKYDcnO6iLKPgHRJ11aVGkgt2M2IiWlobzKzyB4zBwBGvBwIdc7rnjR8ITLqGU6qm%2FC8hms%2Bo39TWgg0fUUJX7PV7RlVo1Qtsm4Ckhl%2BM4A%2BGY%2BQJ8TCYiLmp1m4AHWbyGa%2BxiFEtQMneIY80RTtCyCahJwDpOsYc0FtHGDS5wbjjDp03AM6aRRhIpTGAbH5jBEJIm208UwMcbmuIFHam34KEZ0tYGNgEeXFEUW9jAKvLYRFHjCs8moIEMZpHRxnNwzJrx3Oj%2Fj2a%2BQbT4b%2BDBjcmz6iK9M6LF76I6UnHY%2Fgc%2BythB2YK%2B1tcC%2Ful54COPMeT0tsMBxsW%2B0dY52VPAe1RAgAoGw7OA8QoUbvVzgPEU7qS2HJ4b33tRQfBTQBcl4wBZgMIlhrX5EVxJbd7YU0JXD%2BipLw8sG8DBrCLzAAAAAElFTkSuQmCC) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABYklEQVR42t3VP0vDQBjH8Tq4SKXiWFwcRHARVyspgq9AOjkoLoKTcymk%2BjKKYDcnO6iLKPgHRJ11aVGkgt2M2IiWlobzKzyB4zBwBGvBwIdc7rnjR8ITLqGU6qm%2FC8hms%2Bo39TWgg0fUUJX7PV7RlVo1Qtsm4Ckhl%2BM4A%2BGY%2BQJ8TCYiLmp1m4AHWbyGa%2BxiFEtQMneIY80RTtCyCahJwDpOsYc0FtHGDS5wbjjDp03AM6aRRhIpTGAbH5jBEJIm208UwMcbmuIFHam34KEZ0tYGNgEeXFEUW9jAKvLYRFHjCs8moIEMZpHRxnNwzJrx3Oj%2Fj2a%2BQbT4b%2BDBjcmz6iK9M6LF76I6UnHY%2Fgc%2BythB2YK%2B1tcC%2Ful54COPMeT0tsMBxsW%2B0dY52VPAe1RAgAoGw7OA8QoUbvVzgPEU7qS2HJ4b33tRQfBTQBcl4wBZgMIlhrX5EVxJbd7YU0JXD%2BipLw8sG8DBrCLzAAAAAElFTkSuQmCC); } .icon_man_24 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABLUlEQVR42t3VsUrDUBSAYaWtSwSp6NAH6CbOVSHZ9QEEBx9AEPc6OAh9Bp0KDg6igqs4tktXFQQnKQiKiNRAp%2BT4C2cImqbXc3Fp4VsazvlpuUmmRORfTUig6BNFURnbeISoB2yhlDPiHvhegF18Qn74wA5KPoE13EFGuEXDFGBwBoeQMQ5QsQSqOHUInGDOEljElUPgEguWwDzOHQJnqFoCszh2CBwhsAQq2EdSsDxFE2XrMV3HS0HgFRs%2B90ENNwWBa9TMgTAMp%2FWR8J6z%2FA2bXo8K%2FRXLuB9xFy%2BZAwwHWMUF4pxArNdWnE%2BRLm2ghS76SMecoj66aOls8CvAl3XsoYNnDCF%2FNNTZju6qZwO93L%2FBLkYvGxggVeIpVYNsoI0nJBBPie5qT8ZL%2FwsLLryq%2FnnTDAAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABLUlEQVR42t3VsUrDUBSAYaWtSwSp6NAH6CbOVSHZ9QEEBx9AEPc6OAh9Bp0KDg6igqs4tktXFQQnKQiKiNRAp%2BT4C2cImqbXc3Fp4VsazvlpuUmmRORfTUig6BNFURnbeISoB2yhlDPiHvhegF18Qn74wA5KPoE13EFGuEXDFGBwBoeQMQ5QsQSqOHUInGDOEljElUPgEguWwDzOHQJnqFoCszh2CBwhsAQq2EdSsDxFE2XrMV3HS0HgFRs%2B90ENNwWBa9TMgTAMp%2FWR8J6z%2FA2bXo8K%2FRXLuB9xFy%2BZAwwHWMUF4pxArNdWnE%2BRLm2ghS76SMecoj66aOls8CvAl3XsoYNnDCF%2FNNTZju6qZwO93L%2FBLkYvGxggVeIpVYNsoI0nJBBPie5qT8ZL%2FwsLLryq%2FnnTDAAAAABJRU5ErkJggg%3D%3D); } .icon_keyhole_24 { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABIUlEQVR42rXUP0vCQRjA8bIoiAppLmwQEmehSX5RLTb2Huw9RGODLvUCXG0Kp16ALhENNRjRXLRlIeafJOyevkNu14M89%2FPgMx133%2BF4bkZEpkrdHK8oipIoowuHK2QxGxzgkk1cYgD3R9BEAQlzgMOruMA3xOMZuZDAEToQxRkWrYEHOIiiibQ1IBMEOtixBn4mCLSRtwbqIKIGbrFhDRzgA6I4xpw1kMA5ep6LHa6RCh20ddx4Aq%2FYj2OSF1D3BJ6QjCOQwosn8IbtOAK7%2BPQEvlAcf3ghj1zE4J9HrmApJDCPkvLZ3WMtJLCCmjLNLWRCAmk8KkPWxyHMgQLaSmCIk5BJPkVfCYxQxbI10MBQCTjcYcsaeMcIomhhTwtM1S9g12NLa1YPRQAAAABJRU5ErkJggg%3D%3D) + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABIUlEQVR42rXUP0vCQRjA8bIoiAppLmwQEmehSX5RLTb2Huw9RGODLvUCXG0Kp16ALhENNRjRXLRlIeafJOyevkNu14M89%2FPgMx133%2BF4bkZEpkrdHK8oipIoowuHK2QxGxzgkk1cYgD3R9BEAQlzgMOruMA3xOMZuZDAEToQxRkWrYEHOIiiibQ1IBMEOtixBn4mCLSRtwbqIKIGbrFhDRzgA6I4xpw1kMA5ep6LHa6RCh20ddx4Aq%2FYj2OSF1D3BJ6QjCOQwosn8IbtOAK7%2BPQEvlAcf3ghj1zE4J9HrmApJDCPkvLZ3WMtJLCCmjLNLWRCAmk8KkPWxyHMgQLaSmCIk5BJPkVfCYxQxbI10MBQCTjcYcsaeMcIomhhTwtM1S9g12NLa1YPRQAAAABJRU5ErkJggg%3D%3D); } .iub_base_container { border-radius: 3px; background: #ffffff; - color: #6B6B6B; - position: relative + color: #6b6b6b; + position: relative; } - .iub_base_container>.close { - background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAYCAMAAAAmopZHAAAAw1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4eHh1dXUAAAAAAAAAAAAAAAAAAABfX18AAAAAAAAAAAAAAAAAAAAAAAA2NjYAAAAAAAArKyvJycne3t7X19eFhYWxsbGVlZWsrKzr6%2BvLy8vJycnv7%2B%2Fp6enS0tLi4uL09PTv7%2B%2F8%2FPz7%2B%2Fv4%2BPj39%2FcAAABPT09fX19vb2%2F%2F%2F%2F9S%2BfXQAAAAPHRSTlMAAgMEBQYHCgsMDQ4PEhMWGRobHB8gIiMkJScoKSs0NT1DRUpMWF5gjpOYmaGjpr%2FIys3S1dnZ7vP09vfFQC13AAAA9ElEQVR42oXQZ6%2BCMBiG4aeCAoqiuPceuPes8P7%2FX6Xn5RgBTbw%2BNO3dpG2KH0RcM5JJQ4uLUE2UnSM9HZ1y4r0TM50z%2FTs7ZuyVSysKWJX8DZHeUsg2zUfpU4qY6gBE8xLtl6YAtAnP79Ij8uSdFxMNsHY8lVK67nPgxc4CisQ8yTxiRaBGPvcvu%2BSrAY1vvQHUv51TByqHz3sPFcCef75zbgOpwZUiroMUoFSX0b6sKgDMzjqc1x2Tvznb2wTzppf1P1q1u7PTq55mXVuFT7Va48X%2BRnTbL8YtizMTRqHdH45Gw367YAgEKHoml8%2FnMroC9gCKfVabzD1q%2BwAAAABJRU5ErkJggg%3D%3D) no-repeat; + .iub_base_container > .close { + background: transparent + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAYCAMAAAAmopZHAAAAw1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4eHh1dXUAAAAAAAAAAAAAAAAAAABfX18AAAAAAAAAAAAAAAAAAAAAAAA2NjYAAAAAAAArKyvJycne3t7X19eFhYWxsbGVlZWsrKzr6%2BvLy8vJycnv7%2B%2Fp6enS0tLi4uL09PTv7%2B%2F8%2FPz7%2B%2Fv4%2BPj39%2FcAAABPT09fX19vb2%2F%2F%2F%2F9S%2BfXQAAAAPHRSTlMAAgMEBQYHCgsMDQ4PEhMWGRobHB8gIiMkJScoKSs0NT1DRUpMWF5gjpOYmaGjpr%2FIys3S1dnZ7vP09vfFQC13AAAA9ElEQVR42oXQZ6%2BCMBiG4aeCAoqiuPceuPes8P7%2FX6Xn5RgBTbw%2BNO3dpG2KH0RcM5JJQ4uLUE2UnSM9HZ1y4r0TM50z%2FTs7ZuyVSysKWJX8DZHeUsg2zUfpU4qY6gBE8xLtl6YAtAnP79Ij8uSdFxMNsHY8lVK67nPgxc4CisQ8yTxiRaBGPvcvu%2BSrAY1vvQHUv51TByqHz3sPFcCef75zbgOpwZUiroMUoFSX0b6sKgDMzjqc1x2Tvznb2wTzppf1P1q1u7PTq55mXVuFT7Va48X%2BRnTbL8YtizMTRqHdH45Gw367YAgEKHoml8%2FnMroC9gCKfVabzD1q%2BwAAAABJRU5ErkJggg%3D%3D) + no-repeat; border: none; display: block; position: absolute; @@ -791,268 +792,268 @@ top: -10px; right: -9px; height: 24px; - width: 23px + width: 23px; } .iubenda_policy a { font-weight: normal; - border-bottom: 1px solid #F0F0F0 + border-bottom: 1px solid #f0f0f0; } .iub_content { position: relative; padding: 10px 30px; margin: 0 auto; - border-radius: 3px 3px 0 0 + border-radius: 3px 3px 0 0; } #wbars { position: relative; overflow-y: auto; - overflow-x: hidden + overflow-x: hidden; } #wbars .horizontal { - display: none + display: none; } .iub_header { - border-bottom: 1px dotted #DFDFDF; + border-bottom: 1px dotted #dfdfdf; padding-bottom: 25px; - position: relative + position: relative; } .iub_header p { margin: 0; - padding: 0 + padding: 0; } .iub_header img { display: block; position: absolute; top: 5px; - right: 0 + right: 0; } h1, h2, h3 { - color: #3F3F3F; - margin: 0 + color: #3f3f3f; + margin: 0; } - h1+p, - h2+p, - h3+p { - padding-top: 5px + h1 + p, + h2 + p, + h3 + p { + padding-top: 5px; } h1 { font-size: 19px; font-weight: normal; line-height: 23px; - margin-bottom: 5px + margin-bottom: 5px; } h2 { font-size: 17px; font-weight: bold; line-height: 21px; - padding-top: 21px + padding-top: 21px; } h3 { font-size: 13px; line-height: 19px; font-weight: bold; - padding-top: 24px + padding-top: 24px; } - h3+p { - padding-top: 0 + h3 + p { + padding-top: 0; } .iconed ul li h3 { padding-top: 10px; - color: #615e5e + color: #615e5e; } h4 { font-size: 13px; font-weight: bold; padding-top: 19px; - margin-bottom: 0 + margin-bottom: 0; } h4:first-child { - padding-top: 0 + padding-top: 0; } ul.for_boxes, - ul.for_boxes>li, + ul.for_boxes > li, ul.unstyled, - ul.unstyled>li { + ul.unstyled > li { list-style: none; padding: 0; - margin: 0 + margin: 0; } ul.for_boxes { - zoom: 1 + zoom: 1; } ul.for_boxes:before, ul.for_boxes:after { display: table; - content: ""; + content: ''; zoom: 1; - *display: inline + display: inline; } ul.for_boxes:after { - clear: both + clear: both; } .half_col { float: left; width: 50%; - zoom: 1 + zoom: 1; } .half_col:before, .half_col:after { display: table; - content: ""; + content: ''; zoom: 1; - *display: inline + display: inline; } .half_col:after { - clear: both + clear: both; } - .half_col:nth-child(2n+1)>* { - margin-right: 15px + .half_col:nth-child(2n + 1) > * { + margin-right: 15px; } - .half_col:nth-child(2n)>* { - margin-left: 15px + .half_col:nth-child(2n) > * { + margin-left: 15px; } - .half_col+.one_line_col, - .half_col+.iub_footer { - border-top: 1px dotted #DFDFDF + .half_col + .one_line_col, + .half_col + .iub_footer { + border-top: 1px dotted #dfdfdf; } .one_line_col { zoom: 1; float: left; width: 100%; - border-bottom: 1px dotted #DFDFDF + border-bottom: 1px dotted #dfdfdf; } .one_line_col:before, .one_line_col:after { display: table; - content: ""; + content: ''; zoom: 1; - *display: inline + display: inline; } .one_line_col:after { - clear: both + clear: both; } - .one_line_col>ul.for_boxes>li { + .one_line_col > ul.for_boxes > li { float: left; - width: 50% + width: 50%; } - .one_line_col>ul.for_boxes>li:nth-child(2n+1) { - clear: left + .one_line_col > ul.for_boxes > li:nth-child(2n + 1) { + clear: left; } - .one_line_col>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 15px + .one_line_col > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 15px; } - .one_line_col>ul.for_boxes>li:nth-child(2n) { - clear: right + .one_line_col > ul.for_boxes > li:nth-child(2n) { + clear: right; } - .one_line_col>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 15px + .one_line_col > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 15px; } .one_line_col.wide { - width: 100% + width: 100%; } - .one_line_col.wide>ul.for_boxes>li { + .one_line_col.wide > ul.for_boxes > li { clear: both; - width: 100% + width: 100%; } - .one_line_col.wide>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 0 + .one_line_col.wide > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 0; } - .one_line_col.wide>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 0 + .one_line_col.wide > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 0; } ul.normal_list { list-style: disc; display: block; - padding-top: 11px + padding-top: 11px; } ul.normal_list li { list-style: disc; float: none; line-height: 19px; - margin: 5px 25px + margin: 5px 25px; } - .simple_pp>ul>li { - padding-bottom: 21px + .simple_pp > ul > li { + padding-bottom: 21px; } - .simple_pp>ul>li>ul .iconed { + .simple_pp > ul > li > ul .iconed { padding-left: 40px !important; background-repeat: no-repeat; background-color: transparent; background-position-x: 2px; background-position-y: 26px; - background-position: 2px 26px + background-position: 2px 26px; } - .simple_pp .for_boxes>.one_line_col>ul.for_boxes { - margin-top: 0 + .simple_pp .for_boxes > .one_line_col > ul.for_boxes { + margin-top: 0; } .legal_pp .one_line_col { float: none; border-top: 0; - padding-bottom: 21px + padding-bottom: 21px; } - .legal_pp .one_line_col>ul.for_boxes { - margin-top: 21px + .legal_pp .one_line_col > ul.for_boxes { + margin-top: 21px; } - .legal_pp .one_line_col>ul.for_boxes>li:nth-child(2n+1) { + .legal_pp .one_line_col > ul.for_boxes > li:nth-child(2n + 1) { clear: left; - float: left + float: left; } - .legal_pp .one_line_col>ul.for_boxes>li:nth-child(2n) { + .legal_pp .one_line_col > ul.for_boxes > li:nth-child(2n) { float: right; - clear: right + clear: right; } .legal_pp .definitions { - margin-top: 21px + margin-top: 21px; } .legal_pp .definitions .expand-click.w_icon_24 { @@ -1062,185 +1063,185 @@ background-color: transparent; background-position-x: 5px; background-position-y: 0; - background-position: 5px 0 + background-position: 5px 0; } .legal_pp .definitions .expand-content { padding-left: 5px; - padding-right: 5px + padding-right: 5px; } .wrap p { - display: inline-block + display: inline-block; } .iub_footer { clear: both; position: relative; - font-size: 11px + font-size: 11px; } .iub_footer p { font-size: 11px; - padding: 0 + padding: 0; } .iub_content .iub_footer { - padding: 24px 0 + padding: 24px 0; } .iub_content .iub_footer p:last-of-type { margin: 10px 0; - clear: both + clear: both; } .iub_content .iub_footer .show_comp_link { display: block; - float: right + float: right; } - .iub_container>.iub_footer { + .iub_container > .iub_footer { min-height: 21px; - background-color: #F6F6F6; + background-color: #f6f6f6; color: #717171; padding: 30px; -webkit-box-shadow: 0 -1px 6px #cfcfcf; box-shadow: 0 -1px 6px #cfcfcf; - border-radius: 0 0 3px 3px + border-radius: 0 0 3px 3px; } - .iub_container>.iub_footer>.btn { + .iub_container > .iub_footer > .btn { position: absolute; top: 25px; - right: 30px + right: 30px; } - .iub_container>.iub_footer .btn { + .iub_container > .iub_footer .btn { padding: 0px 24px; - line-height: 29px + line-height: 29px; } - .iub_container>.iub_footer .button-stack { - margin: -4px 0 + .iub_container > .iub_footer .button-stack { + margin: -4px 0; } - .iub_container>.iub_footer .button-stack .btn+.btn { - margin-left: 5px + .iub_container > .iub_footer .button-stack .btn + .btn { + margin-left: 5px; } - .iub_container>.iub_footer img { + .iub_container > .iub_footer img { margin: -4px 3px 0; vertical-align: middle; width: 70px; - height: 25px + height: 25px; } .wide { - width: 150px + width: 150px; } .iubenda_fixed_policy .iub_base_container { - max-width: 800px + max-width: 800px; } .iubenda_fixed_policy .iub_container { margin-left: auto; margin-right: auto; - zoom: 1 + zoom: 1; } .iubenda_fixed_policy .iub_container:before, .iubenda_fixed_policy .iub_container:after { display: table; - content: ""; + content: ''; zoom: 1; - *display: inline + display: inline; } .iubenda_fixed_policy .iub_container:after { - clear: both + clear: both; } .iubenda_fluid_policy #wbars { overflow-y: auto; -webkit-box-shadow: none; box-shadow: none; - height: auto + height: auto; } .iubenda_fluid_policy .iub_container { - margin-bottom: 30px + margin-bottom: 30px; } - .iubenda_fluid_policy .half_col:nth-child(2n+1)>* { - margin-right: 0 + .iubenda_fluid_policy .half_col:nth-child(2n + 1) > * { + margin-right: 0; } - .iubenda_fluid_policy .half_col:nth-child(2n)>* { - margin-left: 0 + .iubenda_fluid_policy .half_col:nth-child(2n) > * { + margin-left: 0; } .iubenda_fluid_policy .one_line_col, .iubenda_fluid_policy .half_col { - width: 100% + width: 100%; } - .iubenda_fluid_policy .one_line_col>ul.for_boxes>li, - .iubenda_fluid_policy .half_col>ul.for_boxes>li { + .iubenda_fluid_policy .one_line_col > ul.for_boxes > li, + .iubenda_fluid_policy .half_col > ul.for_boxes > li { clear: both; - width: 100% + width: 100%; } - .iubenda_fluid_policy .one_line_col>ul.for_boxes>li:nth-child(2n+1)>div, - .iubenda_fluid_policy .half_col>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 0 + .iubenda_fluid_policy .one_line_col > ul.for_boxes > li:nth-child(2n + 1) > div, + .iubenda_fluid_policy .half_col > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 0; } - .iubenda_fluid_policy .one_line_col>ul.for_boxes>li:nth-child(2n)>div, - .iubenda_fluid_policy .half_col>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 0 + .iubenda_fluid_policy .one_line_col > ul.for_boxes > li:nth-child(2n) > div, + .iubenda_fluid_policy .half_col > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 0; } .iubenda_embed_policy .iub_base_container { - background: none + background: none; } - .iubenda_embed_policy .iub_container>.iub_footer { + .iubenda_embed_policy .iub_container > .iub_footer { -webkit-box-shadow: none; box-shadow: none; - border-radius: none + border-radius: none; } .iubenda_embed_policy .expand-click { - cursor: default + cursor: default; } .iubenda_vip_policy.iubenda_terms_policy .iub_base_container { - color: #666 + color: #666; } .iubenda_vip_policy.iubenda_terms_policy h2 { font-size: 24px; - padding-top: 50px + padding-top: 50px; } .iubenda_vip_policy.iubenda_terms_policy h3 { color: #444; font-size: 20px; - padding-top: 45px + padding-top: 45px; } .iubenda_vip_policy.iubenda_terms_policy h4 { font-size: 16px; padding-top: 40px; - color: #555 + color: #555; } .iubenda_vip_policy.iubenda_terms_policy h5 { font-size: 14px; padding-top: 35px; margin-bottom: 0; - color: #666 + color: #666; } .iubenda_vip_policy.iubenda_terms_policy h6 { @@ -1248,42 +1249,42 @@ color: #505050; text-transform: uppercase; padding-top: 32px; - margin-bottom: 0 + margin-bottom: 0; } .iubenda_vip_policy.iubenda_terms_policy .definitions { - margin-top: 60px !important + margin-top: 60px !important; } .iubenda_vip_policy.iubenda_terms_policy .definitions .expand-content { - padding: 25px 15px !important + padding: 25px 15px !important; } .iubenda_vip_policy.iubenda_terms_policy .definitions .expand-content h4 { - font-size: 15px !important + font-size: 15px !important; } .iubenda_vip_policy.iubenda_terms_policy .definitions:before { - content: ""; + content: ''; border-top: 1px dotted rgba(0, 0, 0, 0.1); display: block; margin: 0 -10px; position: relative; - top: -45px + top: -45px; } .iubenda_vip_policy.iubenda_fixed_policy .iub_container { max-width: 660px; - padding-top: 80px + padding-top: 80px; } .iubenda_vip_policy .iub_base_container { - color: #6B6B6B + color: #6b6b6b; } .iubenda_vip_policy p { font-size: 14px; - line-height: 1.6 + line-height: 1.6; } .iubenda_vip_policy .allcaps, @@ -1292,19 +1293,20 @@ font-variant: small-caps !important; font-weight: bold !important; font-size: 16px !important; - font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, sans-serif !important + font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, + sans-serif !important; } .iubenda_vip_policy ul li { font-size: 14px; - line-height: 1.6 + line-height: 1.6; } .iubenda_vip_policy h1 { font-size: 30px; color: #141414; line-height: 1.6; - margin-bottom: 60px + margin-bottom: 60px; } .iubenda_vip_policy h2 { @@ -1312,53 +1314,53 @@ color: #141414; line-height: 1.6; padding-top: 50px; - padding-bottom: 15px + padding-bottom: 15px; } .iubenda_vip_policy h3 { color: #141414; font-size: 16px; line-height: 1.6; - margin-bottom: 10px + margin-bottom: 10px; } .iubenda_vip_policy .legal_pp .one_line_col { - padding-bottom: 50px + padding-bottom: 50px; } - .iubenda_vip_policy .half_col:nth-child(2n+1)>* { - margin-right: 0 + .iubenda_vip_policy .half_col:nth-child(2n + 1) > * { + margin-right: 0; } - .iubenda_vip_policy .half_col:nth-child(2n)>* { - margin-left: 0 + .iubenda_vip_policy .half_col:nth-child(2n) > * { + margin-left: 0; } .iubenda_vip_policy .one_line_col, .iubenda_vip_policy .half_col { - width: 100% + width: 100%; } - .iubenda_vip_policy .one_line_col>ul.for_boxes>li, - .iubenda_vip_policy .half_col>ul.for_boxes>li { + .iubenda_vip_policy .one_line_col > ul.for_boxes > li, + .iubenda_vip_policy .half_col > ul.for_boxes > li { clear: both; - width: 100% + width: 100%; } - .iubenda_vip_policy .one_line_col>ul.for_boxes>li:nth-child(2n+1)>div, - .iubenda_vip_policy .half_col>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 0 + .iubenda_vip_policy .one_line_col > ul.for_boxes > li:nth-child(2n + 1) > div, + .iubenda_vip_policy .half_col > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 0; } - .iubenda_vip_policy .one_line_col>ul.for_boxes>li:nth-child(2n)>div, - .iubenda_vip_policy .half_col>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 0 + .iubenda_vip_policy .one_line_col > ul.for_boxes > li:nth-child(2n) > div, + .iubenda_vip_policy .half_col > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 0; } .iubenda_vip_policy .definitions, .iubenda_vip_policy .iub_footer, .iubenda_vip_policy .for_boxes { - color: #59636D + color: #59636d; } .iubenda_vip_policy .definitions h3, @@ -1370,11 +1372,11 @@ .iubenda_vip_policy .definitions li, .iubenda_vip_policy .iub_footer li, .iubenda_vip_policy .for_boxes li { - font-size: 13px + font-size: 13px; } .iubenda_vip_policy .w_icon_24 { - background-image: none + background-image: none; } .iubenda_vip_policy .box_10.expand .expand-click.w_icon_24 { @@ -1382,8 +1384,8 @@ } .iubenda_vip_policy .box_primary { - -webkit-box-shadow: var(--gauzy-shadow)(0 0 0 / 15%) !important; - box-shadow: var(--gauzy-shadow)(0 0 0 / 15%) !important; + -webkit-box-shadow: var(--gauzy-shadow) (0 0 0 / 15%) !important; + box-shadow: var(--gauzy-shadow) (0 0 0 / 15%) !important; border: unset !important; border-radius: var(--border-radius) !important; background: var(--gauzy-card-4) !important; @@ -1391,10 +1393,10 @@ .iubenda_vip_policy .tc-deactivated h1 { font-size: 20px; - margin-bottom: 10px + margin-bottom: 10px; } .iubenda_vip_policy .legal_pp .one_line_col { - padding-bottom: 21px + padding-bottom: 21px; } -} \ No newline at end of file +} diff --git a/packages/ui-core/src/lib/shared/src/legal/terms-and-conditions/terms-and-conditions.component.scss b/packages/ui-core/src/lib/shared/src/legal/terms-and-conditions/terms-and-conditions.component.scss index ce9c6c6eb8e..309e664955e 100644 --- a/packages/ui-core/src/lib/shared/src/legal/terms-and-conditions/terms-and-conditions.component.scss +++ b/packages/ui-core/src/lib/shared/src/legal/terms-and-conditions/terms-and-conditions.component.scss @@ -1,719 +1,720 @@ @import 'themes'; ::ng-deep { - .term-container { - & nb-auth-block { - max-width: 90%; - } + .term-container { + & nb-auth-block { + max-width: 90%; } + } } ::ng-deep { - .term_and_condition { - background-color: var(--gauzy-card-2); - border-radius: var(--border-radius); - height: calc(100vh - 11rem); - overflow: auto; - font-family: inherit !important; - - h1 { - background: var(--gauzy-card-2); - border-radius: var(--border-radius) var(--border-radius) 0 0; - padding: 10px 30px; - margin: -10px -30px 0; - } - - h2 { - color: var(--gauzy-text-color-2); - font-size: 18px !important; - font-weight: 600 !important; - line-height: 24px; - letter-spacing: 0em; - text-align: left; - } - - h3 { - color: var(--gauzy-text-color-1); - font-size: 16px !important; - font-weight: 600 !important; - line-height: 20px; - letter-spacing: 0em; - text-align: left; - } - - h4 { - color: var(--gauzy-text-color-2); - font-size: 13px !important; - font-weight: 600 !important; - line-height: 16px !important; - letter-spacing: 0em; - text-align: left; - } - - p, - li { - font-size: 12px; - font-weight: 400 !important; - line-height: 16px !important; - letter-spacing: 0em; - text-align: left; - } - } + .term_and_condition { + background-color: var(--gauzy-card-2); + border-radius: var(--border-radius); + height: calc(100vh - 11rem); + overflow: auto; + font-family: inherit !important; + + h1 { + background: var(--gauzy-card-2); + border-radius: var(--border-radius) var(--border-radius) 0 0; + padding: 10px 30px; + margin: -10px -30px 0; + } + + h2 { + color: var(--gauzy-text-color-2); + font-size: 18px !important; + font-weight: 600 !important; + line-height: 24px; + letter-spacing: 0em; + text-align: left; + } + + h3 { + color: var(--gauzy-text-color-1); + font-size: 16px !important; + font-weight: 600 !important; + line-height: 20px; + letter-spacing: 0em; + text-align: left; + } + + h4 { + color: var(--gauzy-text-color-2); + font-size: 13px !important; + font-weight: 600 !important; + line-height: 16px !important; + letter-spacing: 0em; + text-align: left; + } + + p, + li { + font-size: 12px; + font-weight: 400 !important; + line-height: 16px !important; + letter-spacing: 0em; + text-align: left; + } + } } ::ng-deep { - .term_and_condition p { - line-height: 19px; - margin: 0; - padding-top: 11px; - } - - .box_primary { - border: 1px solid #C0C1C1; - border-bottom-color: #A8AAAB; - -webkit-box-shadow: 0 1px 0 #ebebec; - box-shadow: 0 1px 0 #ebebec; - -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); - background: #FFF - } - - .box_10 { - padding: 10px; - border-radius: 3px; - margin-bottom: 15px - } - - .box_10>h4 { - margin-bottom: 0; - font-size: 13px !important - } - - .box_10>.w_icon, - .box_10.expand>.w_icon, - .box_10>.w_icon.expand-click, - .box_10.expand>.w_icon.expand-click { - padding-left: 45px; - background-repeat: no-repeat; - background-color: transparent; - background-position-x: 10px; - background-position-y: 7px; - background-position: 10px 7px - } - - .box_10>.w_icon_16, - .box_10.expand>.w_icon_16, - .box_10>.w_icon_16.expand-click, - .box_10.expand>.w_icon_16.expand-click { - padding-left: 40px; - background-repeat: no-repeat; - background-color: transparent; - background-position-x: 11px; - background-position-y: 11px; - background-position: 11px 11px - } - - .box_10>.w_icon_24, - .box_10.expand>.w_icon_24, - .box_10>.w_icon_24.expand-click, - .box_10.expand>.w_icon_24.expand-click { - padding-left: 45px; - background-repeat: no-repeat; - background-color: transparent; - background-position-x: 10px; - background-position-y: 10px; - background-position: 10px 10px - } - - .term_and_condition footer { - margin-top: 17px; - padding-top: 17px; - border-top: 1px solid #eee - } - - .term_and_condition hr { - padding-top: 15px; - margin: 0 0 15px 0 - } - - .term_and_condition hr.primary { - border: 0; - border-bottom: 1px solid #DFDFDF; - -webkit-box-shadow: 0 1px 0 #f7f7f7; - box-shadow: 0 1px 0 #f7f7f7 - } - - .box_10.expand .expand-click { - margin: -10px; - padding: 12px 25px 13px !important; - } - - .box_10.expand .expand-content { - margin-top: 10px - } - - .box_10.expand .expand-content>*:first-child { - margin-top: 0; - padding-top: 0 - } - - .expand.expanded .expand-click:after, - .box_10.expand.expanded .expand-click:after { - content: ""; - position: absolute; - right: 10px; - top: 19px; - border: 5px; - border-color: transparent; - border-style: solid; - border-top-color: #333B43 - } - - .expand .expand-click, - .box_10.expand .expand-click, - .expand.expanded .expand-click, - .box_10.expand.expanded .expand-click { - border-bottom: 1px dotted #DDD; - margin-bottom: 10px; - -webkit-transition: 0.2s linear all; - transition: 0.2s linear all - } - - .expand.collapsed .expand-click { - border-bottom: 0; - margin-bottom: -10px - } - - .all-collapsed .expand .expand-click { - border-bottom: 0; - margin-bottom: -10px - } - - .all-collapsed .expand-content { - display: none - } - - .iub_container-fluid { - position: relative; - min-width: 940px; - padding-left: 20px; - padding-right: 20px; - zoom: 1 - } - - .iub_container-fluid:before, - .iub_container-fluid:after { - display: table; - content: ""; - zoom: 1; - *display: inline - } - - .iub_container-fluid:after { - clear: both - } - - .iub_container-fluid>.sidebar { - float: left; - width: 220px - } - - .iub_container-fluid>.iub_content { - margin-left: 240px - } - - .iubenda_policy a { - text-decoration: none; - font-weight: bold; - border-bottom: 1px solid #F6F6F6; - color: #333b43 - } - - .iubenda_policy a.unstyled { - border-bottom: 0 - } - - .iubenda_policy a:hover:not(.btn) { - color: #121518; - border-bottom-color: #D6D6D6; - -webkit-transition: 0.1s linear all; - transition: 0.1s linear all - } - - .iubenda_policy a:focus { - outline: none - } - - .iubenda_policy a.no_border, - a.no_border:hover { - border-bottom-width: 0 - } - - .iubenda_policy .pull-right { - float: right - } - - .pull-left { - float: left - } - - .hide { - display: none - } - - .show { - display: block - } - - .link_on_dark a { - border-bottom-color: rgba(247, 247, 247, 0.3) - } - - .link_on_dark a:hover { - border-bottom-color: rgba(247, 247, 247, 0.6) - } - - .iubenda_policy a { - font-weight: normal; - border-bottom: 1px solid #F0F0F0 - } - - .iub_content { - position: relative; - padding: 10px 30px; - margin: 0 auto; - border-radius: 3px 3px 0 0 - } - - #wbars { - position: relative; - overflow-y: auto; - overflow-x: hidden - } - - #wbars .horizontal { - display: none - } - - .iub_header { - border-bottom: 1px dotted #DFDFDF; - padding-bottom: 25px; - position: relative - } - - .iub_header p { - margin: 0; - padding: 0 - } - - .iub_header img { - display: block; - position: absolute; - top: 5px; - right: 0 - } - - .one_line_col { - zoom: 1; - float: left; - width: 100%; - border-bottom: 1px dotted #DFDFDF - } - - .one_line_col:before, - .one_line_col:after { - display: table; - content: ""; - zoom: 1; - *display: inline - } - - .one_line_col:after { - clear: both - } - - .one_line_col>ul.for_boxes>li { - float: left; - width: 50% - } - - .one_line_col>ul.for_boxes>li:nth-child(2n+1) { - clear: left - } - - .one_line_col>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 15px - } - - .one_line_col>ul.for_boxes>li:nth-child(2n) { - clear: right - } - - .one_line_col>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 15px - } - - .one_line_col.wide { - width: 100% - } - - .one_line_col.wide>ul.for_boxes>li { - clear: both; - width: 100% - } - - .one_line_col.wide>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 0 - } - - .one_line_col.wide>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 0 - } - - .legal_pp .one_line_col { - float: none; - border-top: 0; - padding-bottom: 21px - } - - .legal_pp .one_line_col>ul.for_boxes { - margin-top: 21px - } - - .legal_pp .one_line_col>ul.for_boxes>li:nth-child(2n+1) { - clear: left; - float: left - } - - .legal_pp .one_line_col>ul.for_boxes>li:nth-child(2n) { - float: right; - clear: right - } - - .legal_pp .definitions { - margin-top: 21px - } - - .legal_pp .definitions .expand-click.w_icon_24 { - margin-top: -11px; - padding: 14px 10px 12px 45px; - background-repeat: no-repeat; - background-color: transparent; - background-position-x: 5px; - background-position-y: 0; - background-position: 5px 0 - } - - .legal_pp .definitions .expand-content { - padding-left: 5px; - padding-right: 5px - } - - .wrap p { - display: inline-block - } - - .iub_footer { - clear: both; - position: relative; - font-size: 11px - } - - .iub_footer p { - font-size: 11px; - padding: 0 - } - - .iub_content .iub_footer { - padding: 24px 0 - } - - .iub_content .iub_footer p:last-of-type { - margin: 10px 0; - clear: both - } - - .iub_content .iub_footer .show_comp_link { - display: block; - float: right - } - - .iub_container>.iub_footer { - min-height: 21px; - background-color: #F6F6F6; - color: #717171; - padding: 30px; - -webkit-box-shadow: 0 -1px 6px #cfcfcf; - box-shadow: 0 -1px 6px #cfcfcf; - border-radius: 0 0 3px 3px - } - - .iub_container>.iub_footer>.btn { - position: absolute; - top: 25px; - right: 30px - } - - .iub_container>.iub_footer .btn { - padding: 0px 24px; - line-height: 29px - } - - .iub_container>.iub_footer .button-stack { - margin: -4px 0 - } - - .iub_container>.iub_footer .button-stack .btn+.btn { - margin-left: 5px - } - - .iub_container>.iub_footer img { - margin: -4px 3px 0; - vertical-align: middle; - width: 70px; - height: 25px - } - - .wide { - width: 150px - } - - .iubenda_fluid_policy #wbars { - overflow-y: auto; - -webkit-box-shadow: none; - box-shadow: none; - height: auto - } - - .iubenda_fluid_policy .iub_container { - margin-bottom: 30px - } - - .iubenda_fluid_policy .half_col:nth-child(2n+1)>* { - margin-right: 0 - } - - .iubenda_fluid_policy .half_col:nth-child(2n)>* { - margin-left: 0 - } - - .iubenda_fluid_policy .one_line_col, - .iubenda_fluid_policy .half_col { - width: 100% - } - - .iubenda_fluid_policy .one_line_col>ul.for_boxes>li, - .iubenda_fluid_policy .half_col>ul.for_boxes>li { - clear: both; - width: 100% - } - - .iubenda_fluid_policy .one_line_col>ul.for_boxes>li:nth-child(2n+1)>div, - .iubenda_fluid_policy .half_col>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 0 - } - - .iubenda_fluid_policy .one_line_col>ul.for_boxes>li:nth-child(2n)>div, - .iubenda_fluid_policy .half_col>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 0 - } - - .iubenda_embed_policy .iub_base_container { - background: none - } - - .iubenda_embed_policy .iub_container>.iub_footer { - -webkit-box-shadow: none; - box-shadow: none; - border-radius: none - } - - .iubenda_embed_policy .expand-click { - cursor: default - } - - .iubenda_vip_policy.iubenda_terms_policy .iub_base_container { - color: #666 - } - - .iubenda_vip_policy.iubenda_terms_policy h2 { - color: var(--gauzy-text-color-1); - font-size: 18px !important; - font-weight: 600 !important; - line-height: 24px; - letter-spacing: 0em; - text-align: left; - padding-top: 50px - } - - .iubenda_vip_policy.iubenda_terms_policy h3 { - color: var(--gauzy-text-color-2); - font-size: 16px !important; - font-weight: 600 !important; - line-height: 20px; - letter-spacing: 0em; - text-align: left; - padding-top: 45px - } - - .iubenda_vip_policy.iubenda_terms_policy h4 { - font-size: 16px !important; - padding-top: 40px; - color: var(--gauzy-text-color-2); - } - - .iubenda_vip_policy.iubenda_terms_policy h5 { - font-size: 14px; - padding-top: 35px; - margin-bottom: 0; - color: #666 - } - - .iubenda_vip_policy.iubenda_terms_policy h6 { - font-size: 12px; - color: #505050; - text-transform: uppercase; - padding-top: 32px; - margin-bottom: 0 - } - - .iubenda_vip_policy.iubenda_terms_policy .definitions { - margin-top: 60px !important - } - - .iubenda_vip_policy.iubenda_terms_policy .definitions .expand-content { - padding: 25px 15px !important - } - - .iubenda_vip_policy.iubenda_terms_policy .definitions .expand-content h4 { - font-size: 15px !important - } - - .iubenda_vip_policy.iubenda_terms_policy .definitions:before { - content: ""; - border-top: 1px dotted rgba(0, 0, 0, 0.1); - display: block; - margin: 0 -10px; - position: relative; - top: -45px - } - - .iubenda_vip_policy.iubenda_fixed_policy .iub_container { - max-width: 660px; - padding-top: 80px - } - - .iubenda_vip_policy .iub_base_container { - color: #6B6B6B - } - - .iubenda_vip_policy p { - font-size: 14px; - line-height: 1.6 - } - - .iubenda_vip_policy .allcaps, - .iubenda_vip_policy p.allcaps, - .iubenda_vip_policy ul.allcaps li { - font-variant: small-caps !important; - font-weight: bold !important; - font-size: 16px !important; - font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, sans-serif !important - } - - .iubenda_vip_policy ul li { - font-size: 14px; - line-height: 1.6 - } - - .iubenda_vip_policy h1 { - color: var(--gauzy-text-color-1); - font-size: 24px !important; - font-weight: 400 !important; - line-height: 30px; - letter-spacing: 0em; - text-align: left; - margin-bottom: 60px - } - - .iubenda_vip_policy h2 { - color: var(--gauzy-text-color-2); - font-size: 18px !important; - font-weight: 600 !important; - line-height: 24px; - letter-spacing: 0em; - text-align: left; - padding-top: 50px; - padding-bottom: 15px - } - - .iubenda_vip_policy h3 { - color: var(--gauzy-text-color-1); - font-size: 16px !important; - font-weight: 600 !important; - line-height: 20px; - letter-spacing: 0em; - text-align: left; - margin-bottom: 10px - } - - .iubenda_vip_policy .legal_pp .one_line_col { - padding-bottom: 50px - } - - .iubenda_vip_policy .half_col:nth-child(2n+1)>* { - margin-right: 0 - } - - .iubenda_vip_policy .half_col:nth-child(2n)>* { - margin-left: 0 - } - - .iubenda_vip_policy .one_line_col, - .iubenda_vip_policy .half_col { - width: 100% - } - - .iubenda_vip_policy .one_line_col>ul.for_boxes>li, - .iubenda_vip_policy .half_col>ul.for_boxes>li { - clear: both; - width: 100% - } - - .iubenda_vip_policy .one_line_col>ul.for_boxes>li:nth-child(2n+1)>div, - .iubenda_vip_policy .half_col>ul.for_boxes>li:nth-child(2n+1)>div { - margin-right: 0 - } - - .iubenda_vip_policy .one_line_col>ul.for_boxes>li:nth-child(2n)>div, - .iubenda_vip_policy .half_col>ul.for_boxes>li:nth-child(2n)>div { - margin-left: 0 - } - - .iubenda_vip_policy .definitions, - .iubenda_vip_policy .iub_footer, - .iubenda_vip_policy .for_boxes { - color: var(--gauzy-text-color-2); - } - - .iubenda_vip_policy .box_primary { - -webkit-box-shadow: var(--gauzy-shadow)(0 0 0 / 15%) !important; - box-shadow: var(--gauzy-shadow)(0 0 0 / 15%) !important; - border: unset !important; - border-radius: var(--border-radius) !important; - background: var(--gauzy-card-4) !important; - } - - .iubenda_vip_policy .box_primary h3 { - color: var(--gauzy-text-color-1); - font-size: 16px !important; - font-weight: 600 !important; - line-height: 20px; - letter-spacing: 0em; - text-align: left; - } - - .iubenda_vip_policy .legal_pp .one_line_col { - padding-bottom: 21px - } -} \ No newline at end of file + .term_and_condition p { + line-height: 19px; + margin: 0; + padding-top: 11px; + } + + .box_primary { + border: 1px solid #c0c1c1; + border-bottom-color: #a8aaab; + -webkit-box-shadow: 0 1px 0 #ebebec; + box-shadow: 0 1px 0 #ebebec; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + background: #fff; + } + + .box_10 { + padding: 10px; + border-radius: 3px; + margin-bottom: 15px; + } + + .box_10 > h4 { + margin-bottom: 0; + font-size: 13px !important; + } + + .box_10 > .w_icon, + .box_10.expand > .w_icon, + .box_10 > .w_icon.expand-click, + .box_10.expand > .w_icon.expand-click { + padding-left: 45px; + background-repeat: no-repeat; + background-color: transparent; + background-position-x: 10px; + background-position-y: 7px; + background-position: 10px 7px; + } + + .box_10 > .w_icon_16, + .box_10.expand > .w_icon_16, + .box_10 > .w_icon_16.expand-click, + .box_10.expand > .w_icon_16.expand-click { + padding-left: 40px; + background-repeat: no-repeat; + background-color: transparent; + background-position-x: 11px; + background-position-y: 11px; + background-position: 11px 11px; + } + + .box_10 > .w_icon_24, + .box_10.expand > .w_icon_24, + .box_10 > .w_icon_24.expand-click, + .box_10.expand > .w_icon_24.expand-click { + padding-left: 45px; + background-repeat: no-repeat; + background-color: transparent; + background-position-x: 10px; + background-position-y: 10px; + background-position: 10px 10px; + } + + .term_and_condition footer { + margin-top: 17px; + padding-top: 17px; + border-top: 1px solid #eee; + } + + .term_and_condition hr { + padding-top: 15px; + margin: 0 0 15px 0; + } + + .term_and_condition hr.primary { + border: 0; + border-bottom: 1px solid #dfdfdf; + -webkit-box-shadow: 0 1px 0 #f7f7f7; + box-shadow: 0 1px 0 #f7f7f7; + } + + .box_10.expand .expand-click { + margin: -10px; + padding: 12px 25px 13px !important; + } + + .box_10.expand .expand-content { + margin-top: 10px; + } + + .box_10.expand .expand-content > *:first-child { + margin-top: 0; + padding-top: 0; + } + + .expand.expanded .expand-click:after, + .box_10.expand.expanded .expand-click:after { + content: ''; + position: absolute; + right: 10px; + top: 19px; + border: 5px; + border-color: transparent; + border-style: solid; + border-top-color: #333b43; + } + + .expand .expand-click, + .box_10.expand .expand-click, + .expand.expanded .expand-click, + .box_10.expand.expanded .expand-click { + border-bottom: 1px dotted #ddd; + margin-bottom: 10px; + -webkit-transition: 0.2s linear all; + transition: 0.2s linear all; + } + + .expand.collapsed .expand-click { + border-bottom: 0; + margin-bottom: -10px; + } + + .all-collapsed .expand .expand-click { + border-bottom: 0; + margin-bottom: -10px; + } + + .all-collapsed .expand-content { + display: none; + } + + .iub_container-fluid { + position: relative; + min-width: 940px; + padding-left: 20px; + padding-right: 20px; + zoom: 1; + } + + .iub_container-fluid:before, + .iub_container-fluid:after { + display: table; + content: ''; + zoom: 1; + display: inline; + } + + .iub_container-fluid:after { + clear: both; + } + + .iub_container-fluid > .sidebar { + float: left; + width: 220px; + } + + .iub_container-fluid > .iub_content { + margin-left: 240px; + } + + .iubenda_policy a { + text-decoration: none; + font-weight: bold; + border-bottom: 1px solid #f6f6f6; + color: #333b43; + } + + .iubenda_policy a.unstyled { + border-bottom: 0; + } + + .iubenda_policy a:hover:not(.btn) { + color: #121518; + border-bottom-color: #d6d6d6; + -webkit-transition: 0.1s linear all; + transition: 0.1s linear all; + } + + .iubenda_policy a:focus { + outline: none; + } + + .iubenda_policy a.no_border, + a.no_border:hover { + border-bottom-width: 0; + } + + .iubenda_policy .pull-right { + float: right; + } + + .pull-left { + float: left; + } + + .hide { + display: none; + } + + .show { + display: block; + } + + .link_on_dark a { + border-bottom-color: rgba(247, 247, 247, 0.3); + } + + .link_on_dark a:hover { + border-bottom-color: rgba(247, 247, 247, 0.6); + } + + .iubenda_policy a { + font-weight: normal; + border-bottom: 1px solid #f0f0f0; + } + + .iub_content { + position: relative; + padding: 10px 30px; + margin: 0 auto; + border-radius: 3px 3px 0 0; + } + + #wbars { + position: relative; + overflow-y: auto; + overflow-x: hidden; + } + + #wbars .horizontal { + display: none; + } + + .iub_header { + border-bottom: 1px dotted #dfdfdf; + padding-bottom: 25px; + position: relative; + } + + .iub_header p { + margin: 0; + padding: 0; + } + + .iub_header img { + display: block; + position: absolute; + top: 5px; + right: 0; + } + + .one_line_col { + zoom: 1; + float: left; + width: 100%; + border-bottom: 1px dotted #dfdfdf; + } + + .one_line_col:before, + .one_line_col:after { + display: table; + content: ''; + zoom: 1; + display: inline; + } + + .one_line_col:after { + clear: both; + } + + .one_line_col > ul.for_boxes > li { + float: left; + width: 50%; + } + + .one_line_col > ul.for_boxes > li:nth-child(2n + 1) { + clear: left; + } + + .one_line_col > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 15px; + } + + .one_line_col > ul.for_boxes > li:nth-child(2n) { + clear: right; + } + + .one_line_col > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 15px; + } + + .one_line_col.wide { + width: 100%; + } + + .one_line_col.wide > ul.for_boxes > li { + clear: both; + width: 100%; + } + + .one_line_col.wide > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 0; + } + + .one_line_col.wide > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 0; + } + + .legal_pp .one_line_col { + float: none; + border-top: 0; + padding-bottom: 21px; + } + + .legal_pp .one_line_col > ul.for_boxes { + margin-top: 21px; + } + + .legal_pp .one_line_col > ul.for_boxes > li:nth-child(2n + 1) { + clear: left; + float: left; + } + + .legal_pp .one_line_col > ul.for_boxes > li:nth-child(2n) { + float: right; + clear: right; + } + + .legal_pp .definitions { + margin-top: 21px; + } + + .legal_pp .definitions .expand-click.w_icon_24 { + margin-top: -11px; + padding: 14px 10px 12px 45px; + background-repeat: no-repeat; + background-color: transparent; + background-position-x: 5px; + background-position-y: 0; + background-position: 5px 0; + } + + .legal_pp .definitions .expand-content { + padding-left: 5px; + padding-right: 5px; + } + + .wrap p { + display: inline-block; + } + + .iub_footer { + clear: both; + position: relative; + font-size: 11px; + } + + .iub_footer p { + font-size: 11px; + padding: 0; + } + + .iub_content .iub_footer { + padding: 24px 0; + } + + .iub_content .iub_footer p:last-of-type { + margin: 10px 0; + clear: both; + } + + .iub_content .iub_footer .show_comp_link { + display: block; + float: right; + } + + .iub_container > .iub_footer { + min-height: 21px; + background-color: #f6f6f6; + color: #717171; + padding: 30px; + -webkit-box-shadow: 0 -1px 6px #cfcfcf; + box-shadow: 0 -1px 6px #cfcfcf; + border-radius: 0 0 3px 3px; + } + + .iub_container > .iub_footer > .btn { + position: absolute; + top: 25px; + right: 30px; + } + + .iub_container > .iub_footer .btn { + padding: 0px 24px; + line-height: 29px; + } + + .iub_container > .iub_footer .button-stack { + margin: -4px 0; + } + + .iub_container > .iub_footer .button-stack .btn + .btn { + margin-left: 5px; + } + + .iub_container > .iub_footer img { + margin: -4px 3px 0; + vertical-align: middle; + width: 70px; + height: 25px; + } + + .wide { + width: 150px; + } + + .iubenda_fluid_policy #wbars { + overflow-y: auto; + -webkit-box-shadow: none; + box-shadow: none; + height: auto; + } + + .iubenda_fluid_policy .iub_container { + margin-bottom: 30px; + } + + .iubenda_fluid_policy .half_col:nth-child(2n + 1) > * { + margin-right: 0; + } + + .iubenda_fluid_policy .half_col:nth-child(2n) > * { + margin-left: 0; + } + + .iubenda_fluid_policy .one_line_col, + .iubenda_fluid_policy .half_col { + width: 100%; + } + + .iubenda_fluid_policy .one_line_col > ul.for_boxes > li, + .iubenda_fluid_policy .half_col > ul.for_boxes > li { + clear: both; + width: 100%; + } + + .iubenda_fluid_policy .one_line_col > ul.for_boxes > li:nth-child(2n + 1) > div, + .iubenda_fluid_policy .half_col > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 0; + } + + .iubenda_fluid_policy .one_line_col > ul.for_boxes > li:nth-child(2n) > div, + .iubenda_fluid_policy .half_col > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 0; + } + + .iubenda_embed_policy .iub_base_container { + background: none; + } + + .iubenda_embed_policy .iub_container > .iub_footer { + -webkit-box-shadow: none; + box-shadow: none; + border-radius: none; + } + + .iubenda_embed_policy .expand-click { + cursor: default; + } + + .iubenda_vip_policy.iubenda_terms_policy .iub_base_container { + color: #666; + } + + .iubenda_vip_policy.iubenda_terms_policy h2 { + color: var(--gauzy-text-color-1); + font-size: 18px !important; + font-weight: 600 !important; + line-height: 24px; + letter-spacing: 0em; + text-align: left; + padding-top: 50px; + } + + .iubenda_vip_policy.iubenda_terms_policy h3 { + color: var(--gauzy-text-color-2); + font-size: 16px !important; + font-weight: 600 !important; + line-height: 20px; + letter-spacing: 0em; + text-align: left; + padding-top: 45px; + } + + .iubenda_vip_policy.iubenda_terms_policy h4 { + font-size: 16px !important; + padding-top: 40px; + color: var(--gauzy-text-color-2); + } + + .iubenda_vip_policy.iubenda_terms_policy h5 { + font-size: 14px; + padding-top: 35px; + margin-bottom: 0; + color: #666; + } + + .iubenda_vip_policy.iubenda_terms_policy h6 { + font-size: 12px; + color: #505050; + text-transform: uppercase; + padding-top: 32px; + margin-bottom: 0; + } + + .iubenda_vip_policy.iubenda_terms_policy .definitions { + margin-top: 60px !important; + } + + .iubenda_vip_policy.iubenda_terms_policy .definitions .expand-content { + padding: 25px 15px !important; + } + + .iubenda_vip_policy.iubenda_terms_policy .definitions .expand-content h4 { + font-size: 15px !important; + } + + .iubenda_vip_policy.iubenda_terms_policy .definitions:before { + content: ''; + border-top: 1px dotted rgba(0, 0, 0, 0.1); + display: block; + margin: 0 -10px; + position: relative; + top: -45px; + } + + .iubenda_vip_policy.iubenda_fixed_policy .iub_container { + max-width: 660px; + padding-top: 80px; + } + + .iubenda_vip_policy .iub_base_container { + color: #6b6b6b; + } + + .iubenda_vip_policy p { + font-size: 14px; + line-height: 1.6; + } + + .iubenda_vip_policy .allcaps, + .iubenda_vip_policy p.allcaps, + .iubenda_vip_policy ul.allcaps li { + font-variant: small-caps !important; + font-weight: bold !important; + font-size: 16px !important; + font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, + sans-serif !important; + } + + .iubenda_vip_policy ul li { + font-size: 14px; + line-height: 1.6; + } + + .iubenda_vip_policy h1 { + color: var(--gauzy-text-color-1); + font-size: 24px !important; + font-weight: 400 !important; + line-height: 30px; + letter-spacing: 0em; + text-align: left; + margin-bottom: 60px; + } + + .iubenda_vip_policy h2 { + color: var(--gauzy-text-color-2); + font-size: 18px !important; + font-weight: 600 !important; + line-height: 24px; + letter-spacing: 0em; + text-align: left; + padding-top: 50px; + padding-bottom: 15px; + } + + .iubenda_vip_policy h3 { + color: var(--gauzy-text-color-1); + font-size: 16px !important; + font-weight: 600 !important; + line-height: 20px; + letter-spacing: 0em; + text-align: left; + margin-bottom: 10px; + } + + .iubenda_vip_policy .legal_pp .one_line_col { + padding-bottom: 50px; + } + + .iubenda_vip_policy .half_col:nth-child(2n + 1) > * { + margin-right: 0; + } + + .iubenda_vip_policy .half_col:nth-child(2n) > * { + margin-left: 0; + } + + .iubenda_vip_policy .one_line_col, + .iubenda_vip_policy .half_col { + width: 100%; + } + + .iubenda_vip_policy .one_line_col > ul.for_boxes > li, + .iubenda_vip_policy .half_col > ul.for_boxes > li { + clear: both; + width: 100%; + } + + .iubenda_vip_policy .one_line_col > ul.for_boxes > li:nth-child(2n + 1) > div, + .iubenda_vip_policy .half_col > ul.for_boxes > li:nth-child(2n + 1) > div { + margin-right: 0; + } + + .iubenda_vip_policy .one_line_col > ul.for_boxes > li:nth-child(2n) > div, + .iubenda_vip_policy .half_col > ul.for_boxes > li:nth-child(2n) > div { + margin-left: 0; + } + + .iubenda_vip_policy .definitions, + .iubenda_vip_policy .iub_footer, + .iubenda_vip_policy .for_boxes { + color: var(--gauzy-text-color-2); + } + + .iubenda_vip_policy .box_primary { + -webkit-box-shadow: var(--gauzy-shadow) (0 0 0 / 15%) !important; + box-shadow: var(--gauzy-shadow) (0 0 0 / 15%) !important; + border: unset !important; + border-radius: var(--border-radius) !important; + background: var(--gauzy-card-4) !important; + } + + .iubenda_vip_policy .box_primary h3 { + color: var(--gauzy-text-color-1); + font-size: 16px !important; + font-weight: 600 !important; + line-height: 20px; + letter-spacing: 0em; + text-align: left; + } + + .iubenda_vip_policy .legal_pp .one_line_col { + padding-bottom: 21px; + } +} diff --git a/packages/ui-core/src/lib/shared/src/project/project-mutation/project-mutation.component.ts b/packages/ui-core/src/lib/shared/src/project/project-mutation/project-mutation.component.ts index 6999ffe3919..0fcef87d5bf 100644 --- a/packages/ui-core/src/lib/shared/src/project/project-mutation/project-mutation.component.ts +++ b/packages/ui-core/src/lib/shared/src/project/project-mutation/project-mutation.component.ts @@ -26,7 +26,6 @@ import { IIntegrationTenant, IGithubRepository, IOrganizationProjectSetting, - HttpStatus, IIntegrationMapSyncRepository, IOrganizationGithubRepository, SYNC_TAG_GAUZY @@ -530,7 +529,7 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement this.loading = false; const { id: organizationId, tenantId } = this.organization; - const { id: projectId } = this.project; + const { id: projectId, name } = this.project; const integrationId = this.integration['id']; /** */ @@ -548,19 +547,14 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement return this._organizationProjectsService.updateProjectSetting(projectId, { organizationId, tenantId, - repositoryId, + customFields: { repositoryId }, ...(!this.projectSettingForm.get('syncTag').value ? { syncTag: SYNC_TAG_GAUZY } : {}) }); }), - tap((response: any) => { - if (response['status'] == HttpStatus.BAD_REQUEST) { - throw new Error(`${response['message']}`); - } - }), tap(() => { this._toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_PROJECTS.SYNC_REPOSITORY', { repository: repository.full_name, - project: this.project.name + project: name }); }), catchError((error) => { @@ -608,7 +602,7 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement const { id: organizationId, tenantId } = this.organization; // Extract the 'projectId' from the 'project' property. - const { id: projectId } = this.project; + const { id: projectId, name } = this.project; // Create a 'request' object of type 'IOrganizationProjectSetting'. // It contains 'organizationId', 'tenantId', and auto-sync settings taken from 'this.projectSettingForm.value'. @@ -623,18 +617,9 @@ export class ProjectMutationComponent extends TranslationBaseComponent implement this._organizationProjectsService .updateProjectSetting(projectId, request) .pipe( - tap((response: any) => { - if (response['status'] == HttpStatus.BAD_REQUEST) { - throw new Error(`${response['message']}`); - } - }), tap(() => { - this._toastrService.success( - 'NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_PROJECTS.AUTO_SYNC_SETTING', - { - project: this.project.name - } - ); + const message = 'NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_PROJECTS.AUTO_SYNC_SETTING'; + this._toastrService.success(message, { project: name }); }), catchError((error) => { this._errorHandler.handleError(error); diff --git a/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.html b/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.html index f8415896129..96181d0e350 100644 --- a/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.html +++ b/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.html @@ -1,36 +1,26 @@ - + {{ getShortenedName(item.firstName, item.lastName, 42) }}
- + {{ getShortenedName(item.firstName, item.lastName) }} diff --git a/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.ts b/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.ts index 6ab9293db72..924377edc7c 100644 --- a/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.ts +++ b/packages/ui-core/src/lib/shared/src/selectors/employee/employee.component.ts @@ -290,18 +290,23 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } /** - * - * @param employee + * Selects an employee and performs necessary actions based on selection + * @param employee The employee to select */ async selectEmployee(employee: ISelectedEmployee) { - if (!this.skipGlobalChange) { - this._store.selectedEmployee = employee || ALL_EMPLOYEES_SELECTED; - await this.setAttributesToParams({ employeeId: employee?.id }); - } else { - this.selectedEmployee = employee || ALL_EMPLOYEES_SELECTED; - } - if (isNotEmpty(employee)) { - this.selectionChanged.emit(employee); + try { + if (!this.skipGlobalChange) { + this._store.selectedEmployee = employee || ALL_EMPLOYEES_SELECTED; + await this.setAttributesToParams({ employeeId: employee?.id }); + } else { + this.selectedEmployee = employee || ALL_EMPLOYEES_SELECTED; + } + + if (isNotEmpty(employee)) { + this.selectionChanged.emit(employee); + } + } catch (error) { + console.error('Error while selecting employee:', error); } } @@ -314,13 +319,17 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } /** - * - * @param employeeId + * Selects an employee by their ID and performs necessary actions based on the selection + * @param employeeId The ID of the employee to select */ async selectEmployeeById(employeeId: string) { - const employee = this.people.find((employee: ISelectedEmployee) => employeeId === employee.id); - if (employee) { - await this.selectEmployee(employee); + try { + const employee = this.people.find((employee: ISelectedEmployee) => employeeId === employee.id); + if (employee) { + await this.selectEmployee(employee); + } + } catch (error) { + console.error('Error while selecting employee by ID:', error); } } @@ -349,15 +358,20 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } /** - * + * Handles the selection of an employee based on certain conditions */ private onSelectEmployee() { - if (!this.selectedEmployee && isNotEmpty(this.people)) { - // This is so selected employee doesn't get reset when it's already set from somewhere else - this.selectEmployee(this.people[0]); - } - if (!this.defaultSelected && this.selectedEmployee === ALL_EMPLOYEES_SELECTED) { - this.selectedEmployee = null; + try { + if (!this.selectedEmployee && isNotEmpty(this.people)) { + // Ensure selected employee doesn't get reset when already set elsewhere + this.selectEmployee(this.people[0]); + } + + if (!this.defaultSelected && this.selectedEmployee === ALL_EMPLOYEES_SELECTED) { + this.selectedEmployee = null; + } + } catch (error) { + console.error('Error while handling employee selection:', error); } } @@ -400,7 +414,7 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, firstName: employee.user.firstName, lastName: employee.user.lastName, fullName: employee.user.name, - imageUrl: employee.user.imageUrl, + imageUrl: employee.user.image?.fullUrl || employee.user.imageUrl, shortDescription: employee.short_description, employeeLevel: employee.employeeLevel, billRateCurrency: employee.billRateCurrency, @@ -422,12 +436,6 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } }; - ngOnDestroy() { - if (this.people.length > 0 && !this._store.selectedEmployee && !this.skipGlobalChange) { - this._store.selectedEmployee = this.people[0] || ALL_EMPLOYEES_SELECTED; - } - } - /** * Display clearable option in employee selector * @@ -466,4 +474,10 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, this._toastrService.error(error); } }; + + ngOnDestroy() { + if (this.people.length > 0 && !this._store.selectedEmployee && !this.skipGlobalChange) { + this._store.selectedEmployee = this.people[0] || ALL_EMPLOYEES_SELECTED; + } + } } diff --git a/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.html b/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.html index 57e516351bc..ce6f5e208ea 100644 --- a/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.html +++ b/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.html @@ -1,6 +1,6 @@
- - -
{{ value?.fullName }}
-
+ + +
{{ rowData?.customFields?.repository?.fullName }}
+
diff --git a/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.ts b/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.ts index 81848c0588f..1018164ba58 100644 --- a/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.ts +++ b/packages/ui-core/src/lib/shared/src/table-components/github/repository/repository.component.ts @@ -6,11 +6,10 @@ import { Component, Input, OnInit } from '@angular/core'; styleUrls: ['./repository.component.scss'] }) export class GithubRepositoryComponent implements OnInit { - @Input() value: any; @Input() rowData: any; - constructor() { } + constructor() {} - ngOnInit(): void { } + ngOnInit(): void {} } diff --git a/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.html b/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.html index 7a994ef7351..86bfc14f649 100644 --- a/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.html +++ b/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.html @@ -1,13 +1,13 @@ diff --git a/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.ts b/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.ts index e4df73b8cee..be64ac7608a 100644 --- a/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.ts +++ b/packages/ui-core/src/lib/shared/src/table-components/github/resync-button/resync-button.component.ts @@ -3,10 +3,9 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'ngx-resync-button', templateUrl: './resync-button.component.html', - styleUrls: [], + styleUrls: [] }) export class ResyncButtonComponent { - /** * Getter and Setter for managing a dynamic value. */ @@ -50,7 +49,7 @@ export class ResyncButtonComponent { */ onClicked(event: Event) { // Access the repository data from the component's rowData. - const repository = this.rowData.repository; + const repository = this.rowData.customFields?.repository; // Check if the repository data exists and has synchronization enabled. if (!repository || !repository.hasSyncEnabled) { diff --git a/packages/ui-core/src/lib/shared/src/table-components/picture-name-tags/picture-name-tags.component.ts b/packages/ui-core/src/lib/shared/src/table-components/picture-name-tags/picture-name-tags.component.ts index 50c4d263fb4..cc04edccae7 100644 --- a/packages/ui-core/src/lib/shared/src/table-components/picture-name-tags/picture-name-tags.component.ts +++ b/packages/ui-core/src/lib/shared/src/table-components/picture-name-tags/picture-name-tags.component.ts @@ -77,7 +77,8 @@ export class PictureNameTagsComponent extends NotesWithTagsComponent { return { ...this.rowData, id: avatarId || null, - name: fullName || name || null + name: fullName || name || null, + imageUrl: this.rowData.user?.image?.fullUrl || this.rowData.imageUrl }; } diff --git a/packages/ui-core/src/lib/shared/src/table-components/table-components.module.ts b/packages/ui-core/src/lib/shared/src/table-components/table-components.module.ts index d1624615d39..3caf4417f41 100644 --- a/packages/ui-core/src/lib/shared/src/table-components/table-components.module.ts +++ b/packages/ui-core/src/lib/shared/src/table-components/table-components.module.ts @@ -45,6 +45,8 @@ import { ToggleSwitchComponent } from './toggle-switch/toggle-switch.component'; import { TrustHtmlLinkComponent } from './trust-html/trust-html.component'; import { ValueWithUnitComponent } from './value-with-units/value-with-units.component'; import { VisibilityComponent } from './visibility/visibility.component'; +import { DirectivesModule } from '../directives/directives.module'; +import { TaskBadgeViewComponentModule } from '../tasks/task-badge-view/task-badge-view.module'; @NgModule({ imports: [ @@ -56,8 +58,10 @@ import { VisibilityComponent } from './visibility/visibility.component'; NbToggleModule, NbTooltipModule, I18nTranslateModule.forChild(), + DirectivesModule, PipesModule, - ComponentsModule + ComponentsModule, + TaskBadgeViewComponentModule ], declarations: [ AllowScreenshotCaptureComponent, diff --git a/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.html b/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.html index b53d4a1e75e..6d9a177ed26 100644 --- a/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.html +++ b/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.html @@ -250,9 +250,9 @@
- {{ - 'FORM.LABELS.ENABLE_EMPLOYEE_FEATURES' | translate - }} + + {{ 'FORM.LABELS.ENABLE_EMPLOYEE_FEATURES' | translate }} +
diff --git a/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.ts b/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.ts index 24e0b41e887..99091d107d5 100644 --- a/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.ts +++ b/packages/ui-core/src/lib/shared/src/user/forms/basic-info/basic-info-form.component.ts @@ -11,7 +11,8 @@ import { ICandidateSource, ICandidateCreateInput, ICandidate, - IImageAsset + IImageAsset, + IEmployee } from '@gauzy/contracts'; import { filter, firstValueFrom, tap } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; @@ -21,6 +22,7 @@ import { AuthService, CandidatesService, EmployeesService, + ErrorHandlingService, RoleService, CompareDateValidator, UrlPatternValidator @@ -35,10 +37,11 @@ import { FormHelpers } from '../../../forms/helpers'; styleUrls: ['basic-info-form.component.scss'] }) export class BasicInfoFormComponent extends TranslationBaseComponent implements OnInit, AfterViewInit { - @ViewChild('imagePreview') - imagePreviewElement: ElementRef; + FormHelpers: typeof FormHelpers = FormHelpers; + public excludes: RolesEnum[] = []; + public organization: IOrganization; - @Input() public selectedTags: ITag[]; + @Input() public selectedTags: ITag[] = []; /* * Getter & Setter for check is for candidate mutation @@ -74,11 +77,7 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements this.setRoleValidations(value); } - FormHelpers: typeof FormHelpers = FormHelpers; - public excludes: RolesEnum[] = []; - public organization: IOrganization; - - public form: UntypedFormGroup = BasicInfoFormComponent.buildForm(this.fb, this); + public form: UntypedFormGroup = BasicInfoFormComponent.buildForm(this._fb, this); static buildForm(fb: UntypedFormBuilder, self: BasicInfoFormComponent): UntypedFormGroup { return fb.group( { @@ -109,125 +108,161 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements ); } + @ViewChild('imagePreview') imagePreviewElement: ElementRef; + constructor( - private readonly fb: UntypedFormBuilder, - private readonly authService: AuthService, - private readonly roleService: RoleService, - private readonly employeesService: EmployeesService, - private readonly candidatesService: CandidatesService, public readonly translateService: TranslateService, - private readonly store: Store, - private readonly location: Location + private readonly _location: Location, + private readonly _fb: UntypedFormBuilder, + private readonly _authService: AuthService, + private readonly _roleService: RoleService, + private readonly _employeesService: EmployeesService, + private readonly _candidatesService: CandidatesService, + private readonly _store: Store, + private readonly _errorHandlingService: ErrorHandlingService ) { super(translateService); } ngOnInit(): void { this.excludeRoles(); - this.store.selectedOrganization$ + this._store.selectedOrganization$ .pipe( distinctUntilChange(), filter((organization: IOrganization) => !!organization), tap((organization: IOrganization) => (this.organization = organization)), - filter(() => !!this.location.getState()), - tap(() => this.patchUsingLocationState(this.location.getState())), + filter(() => !!this._location.getState()), + tap(() => this.patchUsingLocationState(this._location.getState())), untilDestroyed(this) ) .subscribe(); } /** - * Exclude SUPER_ADMIN role, if don't have permissions + * Excludes the SUPER_ADMIN role if the current user doesn't have the necessary permissions. */ - async excludeRoles() { - const hasSuperAdminRole = await firstValueFrom(this.authService.hasRole([RolesEnum.SUPER_ADMIN])); + async excludeRoles(): Promise { + const hasSuperAdminRole = await firstValueFrom(this._authService.hasRole([RolesEnum.SUPER_ADMIN])); if (!hasSuperAdminRole) { this.excludes.push(RolesEnum.SUPER_ADMIN); } } - public enableEmployee() { - return ( - this.form.get('role').value && - (this.form.get('role').value.name === RolesEnum.SUPER_ADMIN || - this.form.get('role').value.name === RolesEnum.ADMIN) - ); + /** + * Checks if the current form's role is either SUPER_ADMIN or ADMIN. + * + * @returns A boolean indicating whether the role is SUPER_ADMIN or ADMIN. + */ + public enableEmployee(): boolean { + const role = this.form.get('role').value?.name; + return role === RolesEnum.SUPER_ADMIN || role === RolesEnum.ADMIN; } get showImageMeta() { return this.form.get('imageUrl') && this.form.get('imageUrl').value; } + /** + * Registers a user with different roles + * + * @param defaultRoleName - Default role to assign if none is specified + * @param organizationId - ID of the organization + * @param createdById - ID of the user who created this user + * @returns A promise of the created user or employee + */ async registerUser(defaultRoleName: RolesEnum, organizationId?: string, createdById?: string) { if (this.form.invalid) { return; } - const { firstName, lastName, email, username, password } = this.form.value; + const { + firstName, + lastName, + email, + username, + password, tags, imageUrl, imageId, featureAsEmployee, - role: { name } + role: formRole } = this.form.value; + const { tenantId, tenant } = this._store.user; - const { tenantId, tenant } = this.store.user; - /** - * Removed feature organizations from payload, - * which is not necessary to send into the payload - */ - if (tenant.hasOwnProperty('featureOrganizations')) { - delete tenant['featureOrganizations']; - } + // Remove unnecessary featureOrganizations property + delete tenant.featureOrganizations; + + const roleName = formRole?.name || defaultRoleName; + const role: IRole = await this.getRole(roleName, tenantId); - const role: IRole = await firstValueFrom( - this.roleService.getRoleByOptions({ - name: name || defaultRoleName, - tenantId - }) - ); const user: IUser = { - firstName: firstName, - lastName: lastName, - email: email, + firstName, + lastName, + email, username: username || null, - imageUrl: imageUrl, - imageId: imageId, - role: role, - tenant: tenant, - tags: tags + imageUrl, + imageId, + role, + tenant, + tags }; if (role.name === RolesEnum.EMPLOYEE) { - return this.createEmployee(user); + return await this.createEmployee(user); } else if (role.name === RolesEnum.CANDIDATE) { - return this.createCandidate(user); + return await this.createCandidate(user); } else { - if (featureAsEmployee === true) { - return await firstValueFrom( - this.employeesService.create({ - user: user, - organization: this.organization, - password: password - }) - ); - } else { - return await firstValueFrom( - this.authService.register({ - user: user, - password: password, - confirmPassword: password, - organizationId, - createdById - }) - ); - } + return await this.createUser(user, password, organizationId, createdById, featureAsEmployee); } } /** - * Delete existing image + * Creates a user with the specified attributes, either as an employee or a regular user. * + * @param user - The user details. + * @param password - The password for the user. + * @param organizationId - (Optional) The ID of the organization. + * @param createdById - (Optional) The ID of the user who created this user. + * @param featureAsEmployee - (Optional) Whether to create the user as an employee. + * @returns A promise resolving to the created user or employee. + */ + private async createUser( + user: IUser, + password: string, + organizationId?: string, + createdById?: string, + featureAsEmployee?: boolean + ): Promise { + return await firstValueFrom( + this._authService.register({ + user, + password, + confirmPassword: password, + organizationId, + createdById, + featureAsEmployee + }) + ); + } + + /** + * Fetches a role based on the provided role name and tenant ID. + * + * @param roleName - The name of the role to fetch. + * @param tenantId - The ID of the tenant to which the role belongs. + * @returns A promise resolving to the role object. + */ + private async getRole(roleName: RolesEnum, tenantId: string): Promise { + return await firstValueFrom( + this._roleService.getRoleByOptions({ + name: roleName, + tenantId + }) + ); + } + + /** + * Delete existing image */ deleteImageUrl() { this.form.get('imageId').setValue(null); @@ -237,6 +272,11 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements this.form.get('imageUrl').updateValueAndValidity(); } + /** + * Handle selected tags + * + * @param tags An array of tags to set in the form control. + */ selectedTagsHandler(tags: ITag[]) { this.form.get('tags').setValue(tags); this.form.get('tags').updateValueAndValidity(); @@ -269,11 +309,11 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements /** * Upload third party URL as image/avatar * - * @param image + * @param imageUrl The URL of the image to update in the form control. */ updateImageUrl(imageUrl: string) { try { - const imageUrlControl = this.form.get('imageUrl'); + const imageUrlControl = this.form.get('imageUrl') as FormControl; if (imageUrl) { imageUrlControl.enable(); imageUrlControl.setValue(imageUrl); @@ -282,15 +322,20 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements imageUrlControl.disable(); } } catch (error) { - console.log('Error while updating user profile/avatar by third party URL'); + console.error('Error while updating user profile/avatar by third party URL:', error); } } + /** + * Sets up validation for image URL based on image loading status. + */ private _setupLogoUrlValidation() { + // Clear errors on image load this.imagePreviewElement.nativeElement.onload = () => { this.form.get('imageUrl').setErrors(null); }; + // Set error on image load error, if showImageMeta is true this.imagePreviewElement.nativeElement.onerror = () => { if (this.showImageMeta) { this.form.get('imageUrl').setErrors({ invalidUrl: true }); @@ -299,8 +344,9 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements } /** - * On Selection Change - * @param role + * Handle selection change for roles. + * + * @param role The selected role object. */ onSelectionChange(role: IRole) { if (this.isShowRole) { @@ -310,60 +356,65 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements } /** - * SET role field validations + * SET role field validations based on the given value. * - * @param value + * @param value Indicates whether role validation is required (true) or not (false). */ setRoleValidations(value: boolean) { - if (value === true) { - this.form.get('role').setValidators([Validators.required]); + const control = this.form.get('role'); + if (value) { + control.setValidators([Validators.required]); } else { - this.form.get('role').clearValidators(); + control.clearValidators(); } - this.form.get('role').updateValueAndValidity(); + control.updateValueAndValidity(); } /** - * Create employee from user page + * Create an employee from the user page. * - * @param user - * @returns + * @param user The user object containing employee details. + * @returns A promise that resolves to the created employee. */ - async createEmployee(user: IUser) { - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; - + async createEmployee(user: IUser): Promise { + const { id: organizationId, tenantId } = this.organization; const { password, tags } = this.form.value; const { offerDate = null, acceptDate = null, rejectDate = null, startedWorkOn = null } = this.form.value; const employee: IEmployeeCreateInput = { tenantId, user, - startedWorkOn: startedWorkOn, - password: password, + startedWorkOn, + password, organizationId, + organization: { id: organizationId }, offerDate, acceptDate, rejectDate, - tags: tags + tags }; - return await firstValueFrom(this.employeesService.create(employee)); + + try { + // Create the employee using the employeesService + return await firstValueFrom(this._employeesService.create(employee)); + } catch (error) { + // Handle any errors here, e.g., log them or rethrow as needed + this._errorHandlingService.handleError(`Failed to create employee: ${error.message}`); + } } /** - * Create candidate from user page + * Create a candidate from user page. * - * @param user - * @returns + * @param user The IUser object containing candidate's user details. + * @returns A Promise resolving to the created ICandidate object. */ async createCandidate(user: IUser): Promise { - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; - + const { id: organizationId, tenantId } = this.organization; const { password, tags } = this.form.value; const { appliedDate = null, rejectDate = null, source: sourceName = null } = this.form.value; - let source: ICandidateSource = null; + let source: ICandidateSource | null = null; if (sourceName !== null) { source = { name: sourceName, @@ -371,6 +422,7 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements organizationId }; } + const candidate: ICandidateCreateInput = { user, password, @@ -382,7 +434,14 @@ export class BasicInfoFormComponent extends TranslationBaseComponent implements tenantId, organizationId }; - return await firstValueFrom(this.candidatesService.create(candidate)); + + try { + // Create the candidate using the _candidatesService + return await firstValueFrom(this._candidatesService.create(candidate)); + } catch (error) { + // Handle any errors here, e.g., log them or rethrow as needed + this._errorHandlingService.handleError(`Failed to create candidate: ${error.message}`); + } } /** diff --git a/packages/ui-core/src/lib/shared/src/user/forms/delete-confirmation/delete-confirmation.component.ts b/packages/ui-core/src/lib/shared/src/user/forms/delete-confirmation/delete-confirmation.component.ts index dd788e5841a..c86e978bfed 100644 --- a/packages/ui-core/src/lib/shared/src/user/forms/delete-confirmation/delete-confirmation.component.ts +++ b/packages/ui-core/src/lib/shared/src/user/forms/delete-confirmation/delete-confirmation.component.ts @@ -6,31 +6,21 @@ import { NbDialogRef } from '@nebular/theme'; template: ` - +
{{ 'FORM.CONFIRM' | translate }}
{{ 'FORM.DELETE_CONFIRMATION.SURE' | translate }} {{ recordType | translate }} - {{ - 'FORM.DELETE_CONFIRMATION.RECORD' | translate - }}? + {{ 'FORM.DELETE_CONFIRMATION.RECORD' | translate }} ? - @@ -41,9 +31,8 @@ import { NbDialogRef } from '@nebular/theme'; export class DeleteConfirmationComponent { recordType: string; isRecord: boolean = true; - constructor( - protected dialogRef: NbDialogRef - ) {} + + constructor(protected readonly dialogRef: NbDialogRef) {} close() { this.dialogRef.close(); diff --git a/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.html b/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.html index 5d7ebd712e5..737bae43de4 100644 --- a/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.html +++ b/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.html @@ -1,32 +1,20 @@ - -
{{ 'USERS_PAGE.ADD_USER' | translate }}
+ + + +
+ {{ 'USERS_PAGE.ADD_USER' | translate }} +
- + - - diff --git a/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.ts b/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.ts index 0f6f4cdf8d2..2f11f1a6b2f 100644 --- a/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.ts +++ b/packages/ui-core/src/lib/shared/src/user/user-mutation/user-mutation.component.ts @@ -1,9 +1,10 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { RolesEnum, IUser } from '@gauzy/contracts'; +import { RolesEnum, IUser, IOrganization } from '@gauzy/contracts'; import { NbDialogRef } from '@nebular/theme'; import { ToastrService } from '@gauzy/ui-core/core'; -import { Store } from '@gauzy/ui-core/common'; +import { distinctUntilChange, Store } from '@gauzy/ui-core/common'; import { BasicInfoFormComponent } from '../forms/basic-info/basic-info-form.component'; +import { filter, tap } from 'rxjs/operators'; @Component({ selector: 'ga-user-mutation', @@ -11,32 +12,53 @@ import { BasicInfoFormComponent } from '../forms/basic-info/basic-info-form.comp styleUrls: ['./user-mutation.component.scss'] }) export class UserMutationComponent implements OnInit { - @ViewChild('userBasicInfo') - userBasicInfo: BasicInfoFormComponent; + @ViewChild('userBasicInfo') userBasicInfo: BasicInfoFormComponent; + + public organization: IOrganization; constructor( - protected readonly dialogRef: NbDialogRef, - protected readonly store: Store, - private readonly toastrService: ToastrService + private readonly _dialogRef: NbDialogRef, + private readonly _store: Store, + private readonly _toastrService: ToastrService ) {} - ngOnInit(): void {} + ngOnInit(): void { + this._store.selectedOrganization$ + .pipe( + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => (this.organization = organization)) + ) + .subscribe(); + } - closeDialog(user: IUser = null) { - this.dialogRef.close({ user }); + /** + * Closes the dialog and passes the user data if provided. + * + * @param user - The user object to pass when closing the dialog. Defaults to null. + */ + closeDialog(user: IUser = null): void { + this._dialogRef.close({ user }); } - async add() { + /** + * Registers a user with the default role of VIEWER and associates them with the current organization. + * Closes the dialog with the newly registered user or shows an error if the registration fails. + */ + async add(): Promise { + if (!this.organization) { + return; + } + try { - const organization = this.store.selectedOrganization; const user = await this.userBasicInfo.registerUser( - RolesEnum.VIEWER, //TODO: take role from the form. - organization.id, - this.store.userId + RolesEnum.VIEWER, + this.organization.id, + this._store.userId ); this.closeDialog(user); } catch (error) { - this.toastrService.danger(error); + this._toastrService.danger(error); } } } diff --git a/project.json b/project.json index 40933848915..d64e283093c 100644 --- a/project.json +++ b/project.json @@ -15,7 +15,7 @@ "@gauzy/ui-core": { "tags": [] }, - "job-search-ui-plugin": { + "plugin-job-search-ui": { "tags": [] }, "gauzy": { diff --git a/yarn.lock b/yarn.lock index 626279a739d..a66e3147cea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15486,10 +15486,10 @@ cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.2: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== -ckeditor4-angular@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ckeditor4-angular/-/ckeditor4-angular-5.1.0.tgz#2c0c3ad27edb7c74e5b756def0cd1147e4861abe" - integrity sha512-/aLikcbjFKgPryvGhgPcWF646CW/ERRxbAFvK08oAjyXuKNQy8aXZ8r5R9GTX5xd59pSRIarB5otTr7zBNnWYg== +ckeditor4-angular@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ckeditor4-angular/-/ckeditor4-angular-4.0.1.tgz#fb8e3cd815be2fa6ec3aaa20c9970ee5ce5c5247" + integrity sha512-oiPQ9pCBhXhLpja8TGRiVn9puQJ21em6A3+H9Z1uZ8gJRHCptPfdumCvjFqD76Pog8KymbvHl24GBee67nhqMw== dependencies: ckeditor4-integrations-common "^1.0.0" tslib "^2.3.0" @@ -15499,10 +15499,10 @@ ckeditor4-integrations-common@^1.0.0: resolved "https://registry.yarnpkg.com/ckeditor4-integrations-common/-/ckeditor4-integrations-common-1.0.0.tgz#a8afc33b94877b4d01362c7c73aa9eec162b2d20" integrity sha512-OAoQT/gYrHkg0qgzf6MS/rndYhq3SScLVQ3rtXQeuCE8ju7nFHg3qZ7WGA2XpFxcZzsMP6hhugXqdel5vbcC3g== -ckeditor4@^4.23.0: - version "4.24.0" - resolved "https://registry.yarnpkg.com/ckeditor4/-/ckeditor4-4.24.0.tgz#9699ddbce7eaa9cb1063f7f7d75b0ae761106405" - integrity sha512-ShtIqZMMNmP5r8AhZqnysSaONsx+qKjI/zf5AkU9wKxl0yHVw2/CSxWYmdd40u3dMjJR2kOthQ6USahz528lbw== +ckeditor4@4.22.1: + version "4.22.1" + resolved "https://registry.yarnpkg.com/ckeditor4/-/ckeditor4-4.22.1.tgz#a4e79db1bac2ccb9a672c47e601ba4882afaf455" + integrity sha512-Yj4vTHX5YxHwc48gNqUqTm+KLkRr9tuyb4O2VIABu4oKHWRNVIdLdy6vUNe/XNx+RiTavMejfA1MVOU/MxLjqQ== class-transformer@^0.5.1: version "0.5.1"