From 682c46eb9f84ab5f4ee8ee55c0a518c24652110f Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Sun, 15 Dec 2024 18:54:23 +0000 Subject: [PATCH 01/57] feat(project-version): create project version table and init the ui --- .../app/components/dashboard-container.tsx | 14 +- packages/react-ui/src/app/router/index.tsx | 11 ++ .../src/app/routes/project-version/index.tsx | 144 ++++++++++++++++++ .../lib/project-version-api.ts | 18 +++ packages/server/api/src/app/app.ts | 3 + .../src/app/database/database-connection.ts | 2 + ...1734274305139-CreateProjectVersionTable.ts | 67 ++++++++ .../src/app/database/postgres-connection.ts | 2 + .../project-version.controller.ts | 59 +++++++ .../project-version/project-version.entity.ts | 74 +++++++++ .../project-version/project-version.module.ts | 7 + .../project-version.service.ts | 81 ++++++++++ .../server/api/test/helpers/mocks/index.ts | 14 ++ .../project-version/project-version.test.ts | 90 +++++++++++ packages/shared/package.json | 4 +- packages/shared/src/index.ts | 2 + packages/shared/src/lib/file/index.ts | 3 + .../project-version.request.ts | 8 + .../lib/project-version/project-version.ts | 13 ++ 19 files changed, 613 insertions(+), 3 deletions(-) create mode 100644 packages/react-ui/src/app/routes/project-version/index.tsx create mode 100644 packages/react-ui/src/features/project-version/lib/project-version-api.ts create mode 100644 packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts create mode 100644 packages/server/api/src/app/ee/project-version/project-version.controller.ts create mode 100644 packages/server/api/src/app/ee/project-version/project-version.entity.ts create mode 100644 packages/server/api/src/app/ee/project-version/project-version.module.ts create mode 100644 packages/server/api/src/app/ee/project-version/project-version.service.ts create mode 100644 packages/server/api/test/integration/cloud/project-version/project-version.test.ts create mode 100644 packages/shared/src/lib/project-version/project-version.request.ts create mode 100644 packages/shared/src/lib/project-version/project-version.ts diff --git a/packages/react-ui/src/app/components/dashboard-container.tsx b/packages/react-ui/src/app/components/dashboard-container.tsx index 7e88e98041..684f00718e 100644 --- a/packages/react-ui/src/app/components/dashboard-container.tsx +++ b/packages/react-ui/src/app/components/dashboard-container.tsx @@ -1,5 +1,12 @@ import { t } from 'i18next'; -import { AlertCircle, Link2, Logs, Workflow, Wrench } from 'lucide-react'; +import { + AlertCircle, + FileStack, + Link2, + Logs, + Workflow, + Wrench, +} from 'lucide-react'; import { Navigate } from 'react-router-dom'; import { useEmbedding } from '@/components/embed-provider'; @@ -64,6 +71,11 @@ export function DashboardContainer({ children }: DashboardContainerProps) { showInEmbed: true, hasPermission: checkAccess(Permission.READ_APP_CONNECTION), }, + { + to: '/versions', + label: t('Versions'), + icon: FileStack, + }, { to: '/settings', label: t('Settings'), diff --git a/packages/react-ui/src/app/router/index.tsx b/packages/react-ui/src/app/router/index.tsx index 396d0c1451..fc329c2365 100644 --- a/packages/react-ui/src/app/router/index.tsx +++ b/packages/react-ui/src/app/router/index.tsx @@ -56,6 +56,7 @@ import { GlobalConnectionsTable } from '../routes/platform/setup/connections'; import { LicenseKeyPage } from '../routes/platform/setup/license-key'; import TemplatesPage from '../routes/platform/setup/templates'; import UsersPage from '../routes/platform/users'; +import { ProjectVersionsPage } from '../routes/project-version'; import { FlowRunPage } from '../routes/runs/id'; import AlertsPage from '../routes/settings/alerts'; import AppearancePage from '../routes/settings/appearance'; @@ -201,6 +202,16 @@ const routes = [ ), }), + ...ProjectRouterWrapper({ + path: '/versions', + element: ( + + + + + + ), + }), ...ProjectRouterWrapper({ path: '/plans', element: ( diff --git a/packages/react-ui/src/app/routes/project-version/index.tsx b/packages/react-ui/src/app/routes/project-version/index.tsx new file mode 100644 index 0000000000..cfd4017ef4 --- /dev/null +++ b/packages/react-ui/src/app/routes/project-version/index.tsx @@ -0,0 +1,144 @@ +import { useQuery, useMutation } from '@tanstack/react-query'; +import { ColumnDef } from '@tanstack/react-table'; +import { t } from 'i18next'; +import { Trash, Plus, Download } from 'lucide-react'; + +import { ConfirmationDeleteDialog } from '@/components/delete-dialog'; +import { Button } from '@/components/ui/button'; +import { DataTable, RowDataWithActions } from '@/components/ui/data-table'; +import { DataTableColumnHeader } from '@/components/ui/data-table/data-table-column-header'; +import { TableTitle } from '@/components/ui/table-title'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { INTERNAL_ERROR_TOAST, useToast } from '@/components/ui/use-toast'; +import { projectVersionApi } from '@/features/project-version/lib/project-version-api'; +import { formatUtils } from '@/lib/utils'; +import { ProjectVersion } from '@activepieces/shared'; + +const ProjectVersionsPage = () => { + const { toast } = useToast(); + + const { data, isLoading, refetch } = useQuery({ + queryKey: ['project-versions'], + queryFn: () => projectVersionApi.list(), + }); + + const { mutate: deleteProjectVersion, isPending: isDeleting } = useMutation({ + mutationKey: ['delete-project-version'], + mutationFn: (id: string) => projectVersionApi.delete(id), + onSuccess: () => { + refetch(); + toast({ + title: t('Success'), + description: t('Project Version deleted successfully'), + duration: 3000, + }); + }, + onError: () => { + toast(INTERNAL_ERROR_TOAST); + }, + }); + + const columns: ColumnDef>[] = [ + { + accessorKey: 'name', + accessorFn: (row) => row.name, + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.original.name}
, + }, + { + accessorKey: 'created', + accessorFn: (row) => row.created, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {formatUtils.formatDate(new Date(row.original.created))} +
+ ), + }, + { + accessorKey: 'importedBy', + accessorFn: (row) => row.importedBy, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
{row.original.importedBy}
+ ), + }, + ]; + + return ( +
+
+
+ {t('Project Versions')} +
+ {t('View all history of imported project versions')} +
+
+
+ + +
+
+ { + return ( +
+ + + deleteProjectVersion(row.id)} + > + + + + + {t('Delete Version')} + + +
+ ); + }, + ]} + /> +
+ ); +}; + +ProjectVersionsPage.displayName = 'ProjectRolePage'; +export { ProjectVersionsPage }; diff --git a/packages/react-ui/src/features/project-version/lib/project-version-api.ts b/packages/react-ui/src/features/project-version/lib/project-version-api.ts new file mode 100644 index 0000000000..fa25e14cdb --- /dev/null +++ b/packages/react-ui/src/features/project-version/lib/project-version-api.ts @@ -0,0 +1,18 @@ +import { api } from '@/lib/api'; +import { + SeekPage, + CreateProjectVersionRequestBody, + ProjectVersion, +} from '@activepieces/shared'; + +export const projectVersionApi = { + async list() { + return await api.get>(`/v1/project-versions`); + }, + async create(requestBody: CreateProjectVersionRequestBody) { + return await api.post('/v1/project-versions', requestBody); + }, + async delete(id: string) { + return await api.delete(`/v1/project-versions/${id}`); + }, +}; diff --git a/packages/server/api/src/app/app.ts b/packages/server/api/src/app/app.ts index 3ef18e8eca..33ebb605ec 100644 --- a/packages/server/api/src/app/app.ts +++ b/packages/server/api/src/app/app.ts @@ -53,6 +53,7 @@ import { platformPieceModule } from './ee/pieces/platform-piece-module' import { adminPlatformPieceModule } from './ee/platform/admin-platform.controller' import { projectMemberModule } from './ee/project-members/project-member.module' import { projectRoleModule } from './ee/project-role/project-role.module' +import { projectVersionModule } from './ee/project-version/project-version.module' import { projectEnterpriseHooks } from './ee/projects/ee-project-hooks' import { platformProjectModule } from './ee/projects/platform-project-module' import { referralModule } from './ee/referrals/referral.module' @@ -277,6 +278,7 @@ export const setupApp = async (app: FastifyInstance): Promise = await app.register(analyticsModule) await app.register(projectBillingModule) await app.register(projectRoleModule) + await app.register(projectVersionModule) await app.register(globalConnectionModule) setPlatformOAuthService({ service: platformOAuth2Service, @@ -309,6 +311,7 @@ export const setupApp = async (app: FastifyInstance): Promise = await app.register(usageTrackerModule) await app.register(analyticsModule) await app.register(projectRoleModule) + await app.register(projectVersionModule) await app.register(globalConnectionModule) systemJobHandlers.registerJobHandler(SystemJobName.ISSUES_REMINDER, emailService.sendReminderJobHandler) setPlatformOAuthService({ diff --git a/packages/server/api/src/app/database/database-connection.ts b/packages/server/api/src/app/database/database-connection.ts index 9bd2494f3e..b3a5ec2afa 100644 --- a/packages/server/api/src/app/database/database-connection.ts +++ b/packages/server/api/src/app/database/database-connection.ts @@ -26,6 +26,7 @@ import { OtpEntity } from '../ee/otp/otp-entity' import { ProjectMemberEntity } from '../ee/project-members/project-member.entity' import { ProjectPlanEntity } from '../ee/project-plan/project-plan.entity' import { ProjectRoleEntity } from '../ee/project-role/project-role.entity' +import { ProjectVersionEntity } from '../ee/project-version/project-version.entity' import { ReferralEntity } from '../ee/referrals/referral.entity' import { SigningKeyEntity } from '../ee/signing-key/signing-key-entity' import { FileEntity } from '../file/file.entity' @@ -77,6 +78,7 @@ function getEntities(): EntitySchema[] { WorkerMachineEntity, AiProviderEntity, ProjectRoleEntity, + ProjectVersionEntity, ] switch (edition) { diff --git a/packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts b/packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts new file mode 100644 index 0000000000..ecc584d62a --- /dev/null +++ b/packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts @@ -0,0 +1,67 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class CreateProjectVersionTable1734274305139 implements MigrationInterface { + name = 'CreateProjectVersionTable1734274305139' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "project_version" ( + "id" character varying(21) NOT NULL, + "created" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updated" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "projectId" character varying NOT NULL, + "importedAt" character varying NOT NULL, + "importedBy" character varying(21), + "fileId" character varying NOT NULL, + CONSTRAINT "PK_249c24f66d8f7e8ea6f9ff462fb" PRIMARY KEY ("id") + ) + `) + await queryRunner.query(` + CREATE INDEX "idx_project_version_project_id" ON "project_version" ("projectId") + `) + await queryRunner.query(` + CREATE INDEX "idx_project_version_imported_by" ON "project_version" ("importedBy") + `) + await queryRunner.query(` + CREATE INDEX "idx_project_version_file_id" ON "project_version" ("fileId") + `) + await queryRunner.query(` + ALTER TABLE "project_version" + ADD CONSTRAINT "fk_project_version_project_id" FOREIGN KEY ("projectId") REFERENCES "project"("id") ON DELETE CASCADE ON UPDATE NO ACTION + `) + await queryRunner.query(` + ALTER TABLE "project_version" + ADD CONSTRAINT "fk_project_version_imported_by" FOREIGN KEY ("importedBy") REFERENCES "user"("id") ON DELETE + SET NULL ON UPDATE NO ACTION + `) + await queryRunner.query(` + ALTER TABLE "project_version" + ADD CONSTRAINT "fk_project_version_file_id" FOREIGN KEY ("fileId") REFERENCES "file"("id") ON DELETE CASCADE ON UPDATE NO ACTION + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "project_version" DROP CONSTRAINT "fk_project_version_file_id" + `) + await queryRunner.query(` + ALTER TABLE "project_version" DROP CONSTRAINT "fk_project_version_imported_by" + `) + await queryRunner.query(` + ALTER TABLE "project_version" DROP CONSTRAINT "fk_project_version_project_id" + `) + await queryRunner.query(` + DROP INDEX "public"."idx_project_version_file_id" + `) + await queryRunner.query(` + DROP INDEX "public"."idx_project_version_imported_by" + `) + await queryRunner.query(` + DROP INDEX "public"."idx_project_version_project_id" + `) + await queryRunner.query(` + DROP TABLE "project_version" + `) + } + +} diff --git a/packages/server/api/src/app/database/postgres-connection.ts b/packages/server/api/src/app/database/postgres-connection.ts index ad8a210f92..32e6a719ed 100644 --- a/packages/server/api/src/app/database/postgres-connection.ts +++ b/packages/server/api/src/app/database/postgres-connection.ts @@ -165,6 +165,7 @@ import { AddGlobalConnectionsAndRbacForPlatform1731532843905 } from './migration import { AddAuditLogIndicies1731711188507 } from './migration/postgres/1731711188507-AddAuditLogIndicies' import { AddIndiciesToRunAndTriggerData1732324567513 } from './migration/postgres/1732324567513-AddIndiciesToRunAndTriggerData' import { AddProjectRelationInUserInvitation1732790412900 } from './migration/postgres/1732790673766-AddProjectRelationInUserInvitation' +import { CreateProjectVersionTable1734274305139 } from './migration/postgres/1734274305139-CreateProjectVersionTable' const getSslConfig = (): boolean | TlsOptions => { const useSsl = system.get(AppSystemProp.POSTGRES_USE_SSL) @@ -276,6 +277,7 @@ const getMigrations = (): (new () => MigrationInterface)[] => { AddGlobalConnectionsAndRbacForPlatform1731532843905, AddIndiciesToRunAndTriggerData1732324567513, AddProjectRelationInUserInvitation1732790412900, + CreateProjectVersionTable1734274305139, ] const edition = system.getEdition() diff --git a/packages/server/api/src/app/ee/project-version/project-version.controller.ts b/packages/server/api/src/app/ee/project-version/project-version.controller.ts new file mode 100644 index 0000000000..6b7e10ed14 --- /dev/null +++ b/packages/server/api/src/app/ee/project-version/project-version.controller.ts @@ -0,0 +1,59 @@ +import { ApId, CreateProjectVersionRequestBody, ProjectVersion, SeekPage } from '@activepieces/shared' +import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' +import { StatusCodes } from 'http-status-codes' +import { projectVersionService } from './project-version.service' + +export const projectVersionController: FastifyPluginAsyncTypebox = async (app) => { + app.get('/', ListProjectVersionsRequest, async (req) => { + return projectVersionService.list({ + projectId: req.principal.projectId, + }) + }) + + app.post('/', CreateProjectVersionRequest, async (req, res) => { + const projectVersion = await projectVersionService.create({ + projectId: req.principal.projectId, + fileId: req.body.fileId, + importedBy: req.principal.id, + }) + return res.status(StatusCodes.CREATED).send(projectVersion) + // TODO: we should audit log this event + }) + + app.delete('/:id', DeleteProjectVersionRequest, async (req, res) => { + await projectVersionService.delete({ + id: req.params.id, + projectId: req.principal.projectId, + }) + return res.status(StatusCodes.NO_CONTENT).send() + }) + +} + +const ListProjectVersionsRequest = { + schema: { + response: { + [StatusCodes.OK]: SeekPage(ProjectVersion), + }, + }, +} + +const CreateProjectVersionRequest = { + schema: { + body: CreateProjectVersionRequestBody, + response: { + [StatusCodes.CREATED]: ProjectVersion, + }, + }, +} + +const DeleteProjectVersionRequest = { + schema: { + params: Type.Object({ + id: ApId, + }), + response: { + [StatusCodes.NO_CONTENT]: Type.Null(), + }, + }, +} \ No newline at end of file diff --git a/packages/server/api/src/app/ee/project-version/project-version.entity.ts b/packages/server/api/src/app/ee/project-version/project-version.entity.ts new file mode 100644 index 0000000000..bc1cbc244f --- /dev/null +++ b/packages/server/api/src/app/ee/project-version/project-version.entity.ts @@ -0,0 +1,74 @@ +import { File, Project, ProjectVersion, User } from '@activepieces/shared' +import { EntitySchema } from 'typeorm' +import { ApIdSchema, BaseColumnSchemaPart } from '../../database/database-common' + +export type ProjectVersionSchema = ProjectVersion & { + user: User + project: Project + file: File +} + +export const ProjectVersionEntity = new EntitySchema({ + name: 'project_version', + columns: { + ...BaseColumnSchemaPart, + projectId: { + type: String, + nullable: false, + }, + name: { + type: String, + nullable: false, + }, + description: { + type: String, + nullable: true, + }, + importedBy: { + ...ApIdSchema, + nullable: true, + }, + fileId: { + type: String, + nullable: false, + }, + }, + indices: [ + { + name: 'idx_project_version_project_id', + columns: ['projectId'], + }, + ], + relations: { + project: { + type: 'many-to-one', + target: 'project', + cascade: true, + onDelete: 'CASCADE', + joinColumn: { + name: 'projectId', + foreignKeyConstraintName: 'fk_project_version_project_id', + }, + }, + user: { + type: 'many-to-one', + target: 'user', + cascade: true, + onDelete: 'SET NULL', + joinColumn: { + name: 'importedBy', + foreignKeyConstraintName: 'fk_project_version_imported_by', + }, + }, + file: { + type: 'many-to-one', + target: 'file', + cascade: true, + onDelete: 'CASCADE', + joinColumn: { + name: 'fileId', + foreignKeyConstraintName: 'fk_project_version_file_id', + }, + }, + }, +}) diff --git a/packages/server/api/src/app/ee/project-version/project-version.module.ts b/packages/server/api/src/app/ee/project-version/project-version.module.ts new file mode 100644 index 0000000000..b884dccf72 --- /dev/null +++ b/packages/server/api/src/app/ee/project-version/project-version.module.ts @@ -0,0 +1,7 @@ +import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' +import { projectVersionController } from './project-version.controller' + +export const projectVersionModule: FastifyPluginAsyncTypebox = async (app) => { + await app.register(projectVersionController, { prefix: '/v1/project-versions' }) +} + \ No newline at end of file diff --git a/packages/server/api/src/app/ee/project-version/project-version.service.ts b/packages/server/api/src/app/ee/project-version/project-version.service.ts new file mode 100644 index 0000000000..4ac70456b2 --- /dev/null +++ b/packages/server/api/src/app/ee/project-version/project-version.service.ts @@ -0,0 +1,81 @@ +import { ActivepiecesError, ApId, apId, ErrorCode, FileId, ProjectId, ProjectVersion, SeekPage } from '@activepieces/shared' +import { Equal } from 'typeorm' +import { repoFactory } from '../../core/db/repo-factory' +import { ProjectVersionEntity } from './project-version.entity' +export const projectVersionRepo = repoFactory(ProjectVersionEntity) + + +export const projectVersionService = { + async create(params: CreateProjectVersionParams): Promise { + const projectVersionExists = await projectVersionRepo().findOne({ + where: { + fileId: Equal(params.fileId), + }, + }) + if (projectVersionExists) { + throw new ActivepiecesError({ + code: ErrorCode.ENTITY_NOT_FOUND, + params: { entityType: 'project_version', entityId: params.fileId, message: 'Project Version already exists' }, + }) + } + // TODO: uncomment this once we have a implement import project version + // await fileService.getFileOrThrow({ + // fileId: params.fileId, + // projectId: params.projectId, + // type: FileType.PROJECT_VERSION + // }) + + return projectVersionRepo().save({ + id: apId(), + created: new Date().toISOString(), + updated: new Date().toISOString(), + projectId: params.projectId, + importedAt: new Date().toISOString(), + importedBy: params.importedBy, + fileId: params.fileId, + }) + }, + async list({ projectId }: ListParams): Promise> { + const projectVersions = await projectVersionRepo().find({ + where: [ + { + projectId: Equal(projectId), + }, + ], + order: { + created: 'ASC', + }, + }) + + return { + data: await Promise.all(projectVersions.map(async (projectVersion) => { + return { + ...projectVersion, + } + })), + next: null, + previous: null, + } + }, + async delete(params: DeleteProjectVersionParams): Promise { + await projectVersionRepo().delete({ + id: params.id, + projectId: params.projectId, + }) + }, +} + +type ListParams = { + projectId: ProjectId +} + +type CreateProjectVersionParams = { + projectId: ProjectId + fileId: FileId + importedBy: ApId +} + +type DeleteProjectVersionParams = { + id: ApId + projectId: ProjectId +} \ No newline at end of file diff --git a/packages/server/api/test/helpers/mocks/index.ts b/packages/server/api/test/helpers/mocks/index.ts index f6da1f9d45..ada2881c38 100644 --- a/packages/server/api/test/helpers/mocks/index.ts +++ b/packages/server/api/test/helpers/mocks/index.ts @@ -40,6 +40,7 @@ import { Project, ProjectPlan, ProjectRole, + ProjectVersion, RoleType, RunEnvironment, TemplateType, @@ -552,6 +553,19 @@ export const createMockProjectRole = (projectRole?: Partial): Proje } } +export const createMockProjectVersion = (projectVersion?: Partial): ProjectVersion => { + return { + id: projectVersion?.id ?? apId(), + created: projectVersion?.created ?? faker.date.recent().toISOString(), + updated: projectVersion?.updated ?? faker.date.recent().toISOString(), + projectId: projectVersion?.projectId ?? apId(), + importedBy: projectVersion?.importedBy ?? apId(), + fileId: projectVersion?.fileId ?? apId(), + name: projectVersion?.name ?? faker.lorem.word(), + description: projectVersion?.description ?? faker.lorem.sentence(), + } +} + type CreateMockPlatformWithOwnerParams = { platform?: Partial> owner?: Partial> diff --git a/packages/server/api/test/integration/cloud/project-version/project-version.test.ts b/packages/server/api/test/integration/cloud/project-version/project-version.test.ts new file mode 100644 index 0000000000..31c0b0d829 --- /dev/null +++ b/packages/server/api/test/integration/cloud/project-version/project-version.test.ts @@ -0,0 +1,90 @@ +import { PrincipalType, ProjectVersion } from '@activepieces/shared' +import { FastifyInstance } from 'fastify' +import { StatusCodes } from 'http-status-codes' +import { initializeDatabase } from '../../../../src/app/database' +import { databaseConnection } from '../../../../src/app/database/database-connection' +import { setupServer } from '../../../../src/app/server' +import { generateMockToken } from '../../../helpers/auth' +import { createMockFile, createMockProjectVersion, mockBasicSetup } from '../../../helpers/mocks' + +let app: FastifyInstance | null = null + +beforeAll(async () => { + await initializeDatabase({ runMigrations: false }) + app = await setupServer() +}) + +afterAll(async () => { + await databaseConnection().destroy() + await app?.close() +}) + +describe('Project Version API', () => { + describe('Create Project Version', () => { + it('should create a new project version', async () => { + const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockBasicSetup() + const testToken = await generateMockToken({ + type: PrincipalType.USER, + id: mockUserOne.id, + platform: { id: mockPlatformOne.id }, + projectId: mockProjectOne.id, + }) + + const file = createMockFile({ platformId: mockPlatformOne.id, projectId: mockProjectOne.id }) + await databaseConnection().getRepository('file').save(file) + + const projectVersion = createMockProjectVersion({ projectId: mockProjectOne.id, fileId: file.id, importedBy: mockUserOne.id }) + + const response = await app?.inject({ + method: 'POST', + url: '/v1/project-versions', + body: { + fileId: file.id, + importedBy: mockUserOne.id, + }, + headers: { + authorization: `Bearer ${testToken}`, + }, + }) + + expect(response?.statusCode).toBe(StatusCodes.CREATED) + const responseBody = response?.json() as ProjectVersion + expect(responseBody.id).toBeDefined() + expect(responseBody.projectId).toBe(mockProjectOne.id) + expect(responseBody.importedBy).toBe(mockUserOne.id) + expect(responseBody.fileId).toBe(projectVersion.fileId) + expect(responseBody.name).toBe(projectVersion.name) + expect(responseBody.description).toBe(projectVersion.description) + }) + }) + + describe('Delete Project Version', () => { + it('should delete a project version', async () => { + const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockBasicSetup() + const testToken = await generateMockToken({ + type: PrincipalType.USER, + id: mockUserOne.id, + platform: { id: mockPlatformOne.id }, + projectId: mockProjectOne.id, + }) + + const file = createMockFile({ platformId: mockPlatformOne.id, projectId: mockProjectOne.id }) + await databaseConnection().getRepository('file').save(file) + + const projectVersion = createMockProjectVersion({ projectId: mockProjectOne.id, fileId: file.id, importedBy: mockUserOne.id }) + await databaseConnection().getRepository('project_version').save(projectVersion) + + const response = await app?.inject({ + method: 'DELETE', + url: `/v1/project-versions/${projectVersion.id}`, + headers: { + authorization: `Bearer ${testToken}`, + }, + }) + + expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT) + const deletedProjectVersion = await databaseConnection().getRepository('project_version').findOne({ where: { id: projectVersion.id } }) + expect(deletedProjectVersion).toBeNull() + }) + }) +}) \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json index aed2d0cac3..881334c3bb 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,5 +1,5 @@ { "name": "@activepieces/shared", - "version": "0.10.132", + "version": "0.10.133", "type": "commonjs" -} \ No newline at end of file +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8d8a11601f..d944c8fd25 100755 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -73,6 +73,8 @@ export * from './lib/project-role/project-role' export * from './lib/project-role/project-role.request' export * from './lib/flows/operations/migrations' export * from './lib/flow-run/log-serializer' +export * from './lib/project-version/project-version' +export * from './lib/project-version/project-version.request' // Look at https://github.com/sinclairzx81/typebox/issues/350 import { TypeSystemPolicy } from '@sinclair/typebox/system' diff --git a/packages/shared/src/lib/file/index.ts b/packages/shared/src/lib/file/index.ts index dfdf12979b..96299733e9 100644 --- a/packages/shared/src/lib/file/index.ts +++ b/packages/shared/src/lib/file/index.ts @@ -11,6 +11,9 @@ export enum FileType { FLOW_STEP_FILE = 'FLOW_STEP_FILE', SAMPLE_DATA = 'SAMPLE_DATA', TRIGGER_EVENT_FILE = 'TRIGGER_EVENT_FILE', + + // TODO: uncomment this once we have a implement import project version + // PROJECT_VERSION = 'PROJECT_VERSION', } export enum FileCompression { NONE = 'NONE', diff --git a/packages/shared/src/lib/project-version/project-version.request.ts b/packages/shared/src/lib/project-version/project-version.request.ts new file mode 100644 index 0000000000..6c3e81396d --- /dev/null +++ b/packages/shared/src/lib/project-version/project-version.request.ts @@ -0,0 +1,8 @@ +import { Static, Type } from '@sinclair/typebox' + +export const CreateProjectVersionRequestBody = Type.Object({ + fileId: Type.String(), + importedBy: Type.String(), +}) + +export type CreateProjectVersionRequestBody = Static \ No newline at end of file diff --git a/packages/shared/src/lib/project-version/project-version.ts b/packages/shared/src/lib/project-version/project-version.ts new file mode 100644 index 0000000000..debbbd048f --- /dev/null +++ b/packages/shared/src/lib/project-version/project-version.ts @@ -0,0 +1,13 @@ +import { Static, Type } from '@sinclair/typebox' +import { BaseModelSchema } from '../common' + +export const ProjectVersion = Type.Object({ + ...BaseModelSchema, + projectId: Type.String(), + name: Type.String(), + description: Type.Optional(Type.String()), + importedBy: Type.Union([Type.String(), Type.Null()]), + fileId: Type.String(), +}) + +export type ProjectVersion = Static From 8febefccf5f3181bf2acfbe8149bb4d807f82b8a Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Sun, 15 Dec 2024 19:12:09 +0000 Subject: [PATCH 02/57] fix: tests and remove unused imports --- .../server/api/src/app/database/database-connection.ts | 2 -- .../cloud/project-members/project-members.test.ts | 4 ++-- .../cloud/project/platform-project-service.test.ts | 10 ++++++++-- .../api/test/integration/cloud/project/project.test.ts | 3 +-- .../src/app/ee/projects/platform-project-service.ts | 1 + 5 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 packages/server/src/app/ee/projects/platform-project-service.ts diff --git a/packages/server/api/src/app/database/database-connection.ts b/packages/server/api/src/app/database/database-connection.ts index 807fb9ecc2..c899ed6b9f 100644 --- a/packages/server/api/src/app/database/database-connection.ts +++ b/packages/server/api/src/app/database/database-connection.ts @@ -27,8 +27,6 @@ import { ProjectMemberEntity } from '../ee/project-members/project-member.entity import { ProjectPlanEntity } from '../ee/project-plan/project-plan.entity' import { ProjectRoleEntity } from '../ee/project-role/project-role.entity' import { ProjectVersionEntity } from '../ee/project-version/project-version.entity' -import { ReferralEntity } from '../ee/referrals/referral.entity' - import { SigningKeyEntity } from '../ee/signing-key/signing-key-entity' import { FileEntity } from '../file/file.entity' import { FlagEntity } from '../flags/flag.entity' diff --git a/packages/server/api/test/integration/cloud/project-members/project-members.test.ts b/packages/server/api/test/integration/cloud/project-members/project-members.test.ts index d3d0f98a5b..03989177df 100644 --- a/packages/server/api/test/integration/cloud/project-members/project-members.test.ts +++ b/packages/server/api/test/integration/cloud/project-members/project-members.test.ts @@ -29,10 +29,10 @@ beforeAll(async () => { }) beforeEach(async () => { - stripeHelper.getOrCreateCustomer = jest + (stripeHelper as any).getOrCreateCustomer = jest .fn() .mockResolvedValue(faker.string.uuid()) - emailService.sendInvitation = jest.fn() + (emailService).sendInvitation = jest.fn() }) afterAll(async () => { diff --git a/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts b/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts index 31d815d5c2..c415154215 100644 --- a/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts +++ b/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts @@ -42,7 +42,10 @@ describe('Platform Project Service', () => { }) // act - await platformProjectService.hardDelete({ id: mockProject.id }) + if (!app) { + throw new Error("Fastify instance is not initialized"); + } + await platformProjectService(app.log).hardDelete({ id: mockProject.id }) // assert const flowCount = await databaseConnection().getRepository('flow').countBy({ projectId: mockProject.id }) @@ -66,7 +69,10 @@ describe('Platform Project Service', () => { await databaseConnection().getRepository('piece_metadata').save([mockPieceMetadata]) // act - await platformProjectService.hardDelete({ id: mockProject.id }) + if (!app) { + throw new Error("Fastify instance is not initialized"); + } + await platformProjectService(app.log).hardDelete({ id: mockProject.id }) // assert const fileCount = await databaseConnection().getRepository('file').countBy({ projectId: mockProject.id }) diff --git a/packages/server/api/test/integration/cloud/project/project.test.ts b/packages/server/api/test/integration/cloud/project/project.test.ts index 1fe2857e0c..76df246d4a 100644 --- a/packages/server/api/test/integration/cloud/project/project.test.ts +++ b/packages/server/api/test/integration/cloud/project/project.test.ts @@ -40,9 +40,8 @@ afterAll(async () => { await databaseConnection().destroy() await app?.close() }) - beforeEach(async () => { - stripeHelper.getOrCreateCustomer = jest + (stripeHelper as any).getOrCreateCustomer = jest .fn() .mockResolvedValue(faker.string.uuid()) }) diff --git a/packages/server/src/app/ee/projects/platform-project-service.ts b/packages/server/src/app/ee/projects/platform-project-service.ts new file mode 100644 index 0000000000..0519ecba6e --- /dev/null +++ b/packages/server/src/app/ee/projects/platform-project-service.ts @@ -0,0 +1 @@ + \ No newline at end of file From 82769918ad694f7a1902fd3061c730bceffe2d1f Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Tue, 17 Dec 2024 07:20:25 +0000 Subject: [PATCH 03/57] chore: rename project-version to project-release --- .../app/components/dashboard-container.tsx | 8 +-- packages/react-ui/src/app/router/index.tsx | 8 +-- .../index.tsx | 38 +++++------ .../lib/project-release-api.ts | 18 +++++ .../lib/project-version-api.ts | 18 ----- packages/server/api/src/app/app.ts | 6 +- .../src/app/database/database-connection.ts | 4 +- ...1734274305139-CreateProjectVersionTable.ts | 67 ------------------- ...1734418823028-CreateProjectReleaseTable.ts | 56 ++++++++++++++++ .../src/app/database/postgres-connection.ts | 4 +- .../project-release.controller.ts | 61 +++++++++++++++++ .../project-release.entity.ts} | 16 ++--- .../project-release/project-release.module.ts | 7 ++ .../project-release.service.ts} | 43 +++++------- .../project-version.controller.ts | 59 ---------------- .../project-version/project-version.module.ts | 7 -- .../server/api/test/helpers/mocks/index.ts | 20 +++--- .../project-release.test.ts} | 39 ++++++----- packages/shared/src/index.ts | 4 +- .../project-release.request.ts | 10 +++ ...{project-version.ts => project-release.ts} | 6 +- .../project-version.request.ts | 8 --- 22 files changed, 248 insertions(+), 259 deletions(-) rename packages/react-ui/src/app/routes/{project-version => project-release}/index.tsx (77%) create mode 100644 packages/react-ui/src/features/project-version/lib/project-release-api.ts delete mode 100644 packages/react-ui/src/features/project-version/lib/project-version-api.ts delete mode 100644 packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts create mode 100644 packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts create mode 100644 packages/server/api/src/app/ee/project-release/project-release.controller.ts rename packages/server/api/src/app/ee/{project-version/project-version.entity.ts => project-release/project-release.entity.ts} (76%) create mode 100644 packages/server/api/src/app/ee/project-release/project-release.module.ts rename packages/server/api/src/app/ee/{project-version/project-version.service.ts => project-release/project-release.service.ts} (54%) delete mode 100644 packages/server/api/src/app/ee/project-version/project-version.controller.ts delete mode 100644 packages/server/api/src/app/ee/project-version/project-version.module.ts rename packages/server/api/test/integration/cloud/{project-version/project-version.test.ts => project-release/project-release.test.ts} (67%) create mode 100644 packages/shared/src/lib/project-version/project-release.request.ts rename packages/shared/src/lib/project-version/{project-version.ts => project-release.ts} (61%) delete mode 100644 packages/shared/src/lib/project-version/project-version.request.ts diff --git a/packages/react-ui/src/app/components/dashboard-container.tsx b/packages/react-ui/src/app/components/dashboard-container.tsx index 684f00718e..373fbac067 100644 --- a/packages/react-ui/src/app/components/dashboard-container.tsx +++ b/packages/react-ui/src/app/components/dashboard-container.tsx @@ -1,7 +1,7 @@ import { t } from 'i18next'; import { AlertCircle, - FileStack, + Box, Link2, Logs, Workflow, @@ -72,9 +72,9 @@ export function DashboardContainer({ children }: DashboardContainerProps) { hasPermission: checkAccess(Permission.READ_APP_CONNECTION), }, { - to: '/versions', - label: t('Versions'), - icon: FileStack, + to: '/releases', + label: t('Releases'), + icon: Box, }, { to: '/settings', diff --git a/packages/react-ui/src/app/router/index.tsx b/packages/react-ui/src/app/router/index.tsx index fc329c2365..c80a4d53e4 100644 --- a/packages/react-ui/src/app/router/index.tsx +++ b/packages/react-ui/src/app/router/index.tsx @@ -56,7 +56,7 @@ import { GlobalConnectionsTable } from '../routes/platform/setup/connections'; import { LicenseKeyPage } from '../routes/platform/setup/license-key'; import TemplatesPage from '../routes/platform/setup/templates'; import UsersPage from '../routes/platform/users'; -import { ProjectVersionsPage } from '../routes/project-version'; +import { ProjectReleasesPage } from '../routes/project-release'; import { FlowRunPage } from '../routes/runs/id'; import AlertsPage from '../routes/settings/alerts'; import AppearancePage from '../routes/settings/appearance'; @@ -203,11 +203,11 @@ const routes = [ ), }), ...ProjectRouterWrapper({ - path: '/versions', + path: '/releases', element: ( - - + + ), diff --git a/packages/react-ui/src/app/routes/project-version/index.tsx b/packages/react-ui/src/app/routes/project-release/index.tsx similarity index 77% rename from packages/react-ui/src/app/routes/project-version/index.tsx rename to packages/react-ui/src/app/routes/project-release/index.tsx index cfd4017ef4..e3a2b1da35 100644 --- a/packages/react-ui/src/app/routes/project-version/index.tsx +++ b/packages/react-ui/src/app/routes/project-release/index.tsx @@ -14,26 +14,26 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { INTERNAL_ERROR_TOAST, useToast } from '@/components/ui/use-toast'; -import { projectVersionApi } from '@/features/project-version/lib/project-version-api'; +import { projectReleaseApi } from '@/features/project-version/lib/project-release-api'; import { formatUtils } from '@/lib/utils'; -import { ProjectVersion } from '@activepieces/shared'; +import { ProjectRelease } from '@activepieces/shared'; -const ProjectVersionsPage = () => { +const ProjectReleasesPage = () => { const { toast } = useToast(); const { data, isLoading, refetch } = useQuery({ - queryKey: ['project-versions'], - queryFn: () => projectVersionApi.list(), + queryKey: ['project-releases'], + queryFn: () => projectReleaseApi.list(), }); - const { mutate: deleteProjectVersion, isPending: isDeleting } = useMutation({ - mutationKey: ['delete-project-version'], - mutationFn: (id: string) => projectVersionApi.delete(id), + const { mutate: deleteProjectRelease, isPending: isDeleting } = useMutation({ + mutationKey: ['delete-project-release'], + mutationFn: (id: string) => projectReleaseApi.delete(id), onSuccess: () => { refetch(); toast({ title: t('Success'), - description: t('Project Version deleted successfully'), + description: t('Project Release deleted successfully'), duration: 3000, }); }, @@ -42,7 +42,7 @@ const ProjectVersionsPage = () => { }, }); - const columns: ColumnDef>[] = [ + const columns: ColumnDef>[] = [ { accessorKey: 'name', accessorFn: (row) => row.name, @@ -83,9 +83,9 @@ const ProjectVersionsPage = () => {
- {t('Project Versions')} + {t('Project Releases')}
- {t('View all history of imported project versions')} + {t('View all history of imported project releases')}
@@ -111,12 +111,12 @@ const ProjectVersionsPage = () => { deleteProjectVersion(row.id)} + entityName={`${t('Project Release')} ${row.fileId}`} + mutationFn={async () => deleteProjectRelease(row.id)} >
@@ -140,5 +140,5 @@ const ProjectVersionsPage = () => { ); }; -ProjectVersionsPage.displayName = 'ProjectRolePage'; -export { ProjectVersionsPage }; +ProjectReleasesPage.displayName = 'ProjectReleasesPage'; +export { ProjectReleasesPage }; diff --git a/packages/react-ui/src/features/project-version/lib/project-release-api.ts b/packages/react-ui/src/features/project-version/lib/project-release-api.ts new file mode 100644 index 0000000000..e3afb36795 --- /dev/null +++ b/packages/react-ui/src/features/project-version/lib/project-release-api.ts @@ -0,0 +1,18 @@ +import { api } from '@/lib/api'; +import { + SeekPage, + CreateProjectReleaseRequestBody, + ProjectRelease, +} from '@activepieces/shared'; + +export const projectReleaseApi = { + async list() { + return await api.get>(`/v1/project-releases`); + }, + async create(requestBody: CreateProjectReleaseRequestBody) { + return await api.post('/v1/project-releases', requestBody); + }, + async delete(id: string) { + return await api.delete(`/v1/project-releases/${id}`); + }, +}; diff --git a/packages/react-ui/src/features/project-version/lib/project-version-api.ts b/packages/react-ui/src/features/project-version/lib/project-version-api.ts deleted file mode 100644 index fa25e14cdb..0000000000 --- a/packages/react-ui/src/features/project-version/lib/project-version-api.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { api } from '@/lib/api'; -import { - SeekPage, - CreateProjectVersionRequestBody, - ProjectVersion, -} from '@activepieces/shared'; - -export const projectVersionApi = { - async list() { - return await api.get>(`/v1/project-versions`); - }, - async create(requestBody: CreateProjectVersionRequestBody) { - return await api.post('/v1/project-versions', requestBody); - }, - async delete(id: string) { - return await api.delete(`/v1/project-versions/${id}`); - }, -}; diff --git a/packages/server/api/src/app/app.ts b/packages/server/api/src/app/app.ts index 7be681538c..7d0f6d1c01 100644 --- a/packages/server/api/src/app/app.ts +++ b/packages/server/api/src/app/app.ts @@ -53,7 +53,7 @@ import { platformPieceModule } from './ee/pieces/platform-piece-module' import { adminPlatformPieceModule } from './ee/platform/admin-platform.controller' import { projectMemberModule } from './ee/project-members/project-member.module' import { projectRoleModule } from './ee/project-role/project-role.module' -import { projectVersionModule } from './ee/project-version/project-version.module' +import { projectReleaseModule } from './ee/project-release/project-release.module' import { projectEnterpriseHooks } from './ee/projects/ee-project-hooks' import { platformProjectModule } from './ee/projects/platform-project-module' import { signingKeyModule } from './ee/signing-key/signing-key-module' @@ -280,7 +280,7 @@ export const setupApp = async (app: FastifyInstance): Promise = await app.register(analyticsModule) await app.register(projectBillingModule) await app.register(projectRoleModule) - await app.register(projectVersionModule) + await app.register(projectReleaseModule) await app.register(globalConnectionModule) setPlatformOAuthService(platformOAuth2Service(app.log)) projectHooks.set(projectEnterpriseHooks) @@ -311,7 +311,7 @@ export const setupApp = async (app: FastifyInstance): Promise = await app.register(usageTrackerModule) await app.register(analyticsModule) await app.register(projectRoleModule) - await app.register(projectVersionModule) + await app.register(projectReleaseModule) await app.register(globalConnectionModule) systemJobHandlers.registerJobHandler(SystemJobName.ISSUES_REMINDER, emailService(app.log).sendReminderJobHandler) setPlatformOAuthService(platformOAuth2Service(app.log)) diff --git a/packages/server/api/src/app/database/database-connection.ts b/packages/server/api/src/app/database/database-connection.ts index c899ed6b9f..5431e20efb 100644 --- a/packages/server/api/src/app/database/database-connection.ts +++ b/packages/server/api/src/app/database/database-connection.ts @@ -26,7 +26,7 @@ import { OtpEntity } from '../ee/otp/otp-entity' import { ProjectMemberEntity } from '../ee/project-members/project-member.entity' import { ProjectPlanEntity } from '../ee/project-plan/project-plan.entity' import { ProjectRoleEntity } from '../ee/project-role/project-role.entity' -import { ProjectVersionEntity } from '../ee/project-version/project-version.entity' +import { ProjectReleaseEntity } from '../ee/project-release/project-release.entity' import { SigningKeyEntity } from '../ee/signing-key/signing-key-entity' import { FileEntity } from '../file/file.entity' import { FlagEntity } from '../flags/flag.entity' @@ -77,7 +77,7 @@ function getEntities(): EntitySchema[] { WorkerMachineEntity, AiProviderEntity, ProjectRoleEntity, - ProjectVersionEntity, + ProjectReleaseEntity, ] switch (edition) { diff --git a/packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts b/packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts deleted file mode 100644 index ecc584d62a..0000000000 --- a/packages/server/api/src/app/database/migration/postgres/1734274305139-CreateProjectVersionTable.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class CreateProjectVersionTable1734274305139 implements MigrationInterface { - name = 'CreateProjectVersionTable1734274305139' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE "project_version" ( - "id" character varying(21) NOT NULL, - "created" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updated" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "projectId" character varying NOT NULL, - "importedAt" character varying NOT NULL, - "importedBy" character varying(21), - "fileId" character varying NOT NULL, - CONSTRAINT "PK_249c24f66d8f7e8ea6f9ff462fb" PRIMARY KEY ("id") - ) - `) - await queryRunner.query(` - CREATE INDEX "idx_project_version_project_id" ON "project_version" ("projectId") - `) - await queryRunner.query(` - CREATE INDEX "idx_project_version_imported_by" ON "project_version" ("importedBy") - `) - await queryRunner.query(` - CREATE INDEX "idx_project_version_file_id" ON "project_version" ("fileId") - `) - await queryRunner.query(` - ALTER TABLE "project_version" - ADD CONSTRAINT "fk_project_version_project_id" FOREIGN KEY ("projectId") REFERENCES "project"("id") ON DELETE CASCADE ON UPDATE NO ACTION - `) - await queryRunner.query(` - ALTER TABLE "project_version" - ADD CONSTRAINT "fk_project_version_imported_by" FOREIGN KEY ("importedBy") REFERENCES "user"("id") ON DELETE - SET NULL ON UPDATE NO ACTION - `) - await queryRunner.query(` - ALTER TABLE "project_version" - ADD CONSTRAINT "fk_project_version_file_id" FOREIGN KEY ("fileId") REFERENCES "file"("id") ON DELETE CASCADE ON UPDATE NO ACTION - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "project_version" DROP CONSTRAINT "fk_project_version_file_id" - `) - await queryRunner.query(` - ALTER TABLE "project_version" DROP CONSTRAINT "fk_project_version_imported_by" - `) - await queryRunner.query(` - ALTER TABLE "project_version" DROP CONSTRAINT "fk_project_version_project_id" - `) - await queryRunner.query(` - DROP INDEX "public"."idx_project_version_file_id" - `) - await queryRunner.query(` - DROP INDEX "public"."idx_project_version_imported_by" - `) - await queryRunner.query(` - DROP INDEX "public"."idx_project_version_project_id" - `) - await queryRunner.query(` - DROP TABLE "project_version" - `) - } - -} diff --git a/packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts b/packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts new file mode 100644 index 0000000000..b233d5fec6 --- /dev/null +++ b/packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateProjectReleaseTable1734418823028 implements MigrationInterface { + name = 'CreateProjectReleaseTable1734418823028' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "project_release" ( + "id" character varying(21) NOT NULL, + "created" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updated" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "projectId" character varying NOT NULL, + "name" character varying NOT NULL, + "description" character varying, + "importedBy" character varying(21), + "fileId" character varying NOT NULL, + CONSTRAINT "PK_11aa4566a8a7a623e5c3f9809fe" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + CREATE INDEX "idx_project_release_project_id" ON "project_release" ("projectId") + `); + await queryRunner.query(` + ALTER TABLE "project_release" + ADD CONSTRAINT "fk_project_release_project_id" FOREIGN KEY ("projectId") REFERENCES "project"("id") ON DELETE CASCADE ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE "project_release" + ADD CONSTRAINT "fk_project_release_imported_by" FOREIGN KEY ("importedBy") REFERENCES "user"("id") ON DELETE + SET NULL ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE "project_release" + ADD CONSTRAINT "fk_project_release_file_id" FOREIGN KEY ("fileId") REFERENCES "file"("id") ON DELETE CASCADE ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "project_release" DROP CONSTRAINT "fk_project_release_file_id" + `); + await queryRunner.query(` + ALTER TABLE "project_release" DROP CONSTRAINT "fk_project_release_imported_by" + `); + await queryRunner.query(` + ALTER TABLE "project_release" DROP CONSTRAINT "fk_project_release_project_id" + `); + await queryRunner.query(` + DROP INDEX "public"."idx_project_release_project_id" + `); + await queryRunner.query(` + DROP TABLE "project_release" + `); + } + +} diff --git a/packages/server/api/src/app/database/postgres-connection.ts b/packages/server/api/src/app/database/postgres-connection.ts index 32e6a719ed..5b2b012a99 100644 --- a/packages/server/api/src/app/database/postgres-connection.ts +++ b/packages/server/api/src/app/database/postgres-connection.ts @@ -165,7 +165,7 @@ import { AddGlobalConnectionsAndRbacForPlatform1731532843905 } from './migration import { AddAuditLogIndicies1731711188507 } from './migration/postgres/1731711188507-AddAuditLogIndicies' import { AddIndiciesToRunAndTriggerData1732324567513 } from './migration/postgres/1732324567513-AddIndiciesToRunAndTriggerData' import { AddProjectRelationInUserInvitation1732790412900 } from './migration/postgres/1732790673766-AddProjectRelationInUserInvitation' -import { CreateProjectVersionTable1734274305139 } from './migration/postgres/1734274305139-CreateProjectVersionTable' +import { CreateProjectReleaseTable1734418823028 } from './migration/postgres/1734418823028-CreateProjectReleaseTable' const getSslConfig = (): boolean | TlsOptions => { const useSsl = system.get(AppSystemProp.POSTGRES_USE_SSL) @@ -277,7 +277,7 @@ const getMigrations = (): (new () => MigrationInterface)[] => { AddGlobalConnectionsAndRbacForPlatform1731532843905, AddIndiciesToRunAndTriggerData1732324567513, AddProjectRelationInUserInvitation1732790412900, - CreateProjectVersionTable1734274305139, + CreateProjectReleaseTable1734418823028 ] const edition = system.getEdition() diff --git a/packages/server/api/src/app/ee/project-release/project-release.controller.ts b/packages/server/api/src/app/ee/project-release/project-release.controller.ts new file mode 100644 index 0000000000..9b4648753b --- /dev/null +++ b/packages/server/api/src/app/ee/project-release/project-release.controller.ts @@ -0,0 +1,61 @@ +import { ApId, CreateProjectReleaseRequestBody, ProjectRelease, SeekPage } from '@activepieces/shared' +import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' +import { StatusCodes } from 'http-status-codes' +import { projectReleaseService } from './project-release.service' + +export const projectReleaseController: FastifyPluginAsyncTypebox = async (app) => { + app.get('/', ListProjectReleasesRequest, async (req) => { + return projectReleaseService.list({ + projectId: req.principal.projectId, + }) + }) + + app.post('/', CreateProjectReleaseRequest, async (req, res) => { + const projectRelease = await projectReleaseService.create({ + projectId: req.principal.projectId, + fileId: req.body.fileId, + importedBy: req.principal.id, + name: req.body.name, + description: req.body.description, + }) + return res.status(StatusCodes.CREATED).send(projectRelease) + // TODO: we should audit log this event + }) + + app.delete('/:id', DeleteProjectReleaseRequest, async (req, res) => { + await projectReleaseService.delete({ + id: req.params.id, + projectId: req.principal.projectId, + }) + return res.status(StatusCodes.NO_CONTENT).send() + }) + +} + +const ListProjectReleasesRequest = { + schema: { + response: { + [StatusCodes.OK]: SeekPage(ProjectRelease), + }, + }, +} + +const CreateProjectReleaseRequest = { + schema: { + body: CreateProjectReleaseRequestBody, + response: { + [StatusCodes.CREATED]: ProjectRelease, + }, + }, +} + +const DeleteProjectReleaseRequest = { + schema: { + params: Type.Object({ + id: ApId, + }), + response: { + [StatusCodes.NO_CONTENT]: Type.Null(), + }, + }, +} \ No newline at end of file diff --git a/packages/server/api/src/app/ee/project-version/project-version.entity.ts b/packages/server/api/src/app/ee/project-release/project-release.entity.ts similarity index 76% rename from packages/server/api/src/app/ee/project-version/project-version.entity.ts rename to packages/server/api/src/app/ee/project-release/project-release.entity.ts index bc1cbc244f..2635527024 100644 --- a/packages/server/api/src/app/ee/project-version/project-version.entity.ts +++ b/packages/server/api/src/app/ee/project-release/project-release.entity.ts @@ -1,15 +1,15 @@ -import { File, Project, ProjectVersion, User } from '@activepieces/shared' +import { File, Project, ProjectRelease, User } from '@activepieces/shared' import { EntitySchema } from 'typeorm' import { ApIdSchema, BaseColumnSchemaPart } from '../../database/database-common' -export type ProjectVersionSchema = ProjectVersion & { +export type ProjectReleaseSchema = ProjectRelease & { user: User project: Project file: File } -export const ProjectVersionEntity = new EntitySchema({ - name: 'project_version', +export const ProjectReleaseEntity = new EntitySchema({ + name: 'project_release', columns: { ...BaseColumnSchemaPart, projectId: { @@ -35,7 +35,7 @@ export const ProjectVersionEntity = new EntitySchema({ }, indices: [ { - name: 'idx_project_version_project_id', + name: 'idx_project_release_project_id', columns: ['projectId'], }, ], @@ -47,7 +47,7 @@ export const ProjectVersionEntity = new EntitySchema({ onDelete: 'CASCADE', joinColumn: { name: 'projectId', - foreignKeyConstraintName: 'fk_project_version_project_id', + foreignKeyConstraintName: 'fk_project_release_project_id', }, }, user: { @@ -57,7 +57,7 @@ export const ProjectVersionEntity = new EntitySchema({ onDelete: 'SET NULL', joinColumn: { name: 'importedBy', - foreignKeyConstraintName: 'fk_project_version_imported_by', + foreignKeyConstraintName: 'fk_project_release_imported_by', }, }, file: { @@ -67,7 +67,7 @@ export const ProjectVersionEntity = new EntitySchema({ onDelete: 'CASCADE', joinColumn: { name: 'fileId', - foreignKeyConstraintName: 'fk_project_version_file_id', + foreignKeyConstraintName: 'fk_project_release_file_id', }, }, }, diff --git a/packages/server/api/src/app/ee/project-release/project-release.module.ts b/packages/server/api/src/app/ee/project-release/project-release.module.ts new file mode 100644 index 0000000000..12cc5a0267 --- /dev/null +++ b/packages/server/api/src/app/ee/project-release/project-release.module.ts @@ -0,0 +1,7 @@ +import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' +import { projectReleaseController } from './project-release.controller' + +export const projectReleaseModule: FastifyPluginAsyncTypebox = async (app) => { + await app.register(projectReleaseController, { prefix: '/v1/project-releases' }) +} + \ No newline at end of file diff --git a/packages/server/api/src/app/ee/project-version/project-version.service.ts b/packages/server/api/src/app/ee/project-release/project-release.service.ts similarity index 54% rename from packages/server/api/src/app/ee/project-version/project-version.service.ts rename to packages/server/api/src/app/ee/project-release/project-release.service.ts index 4ac70456b2..bc1d4e1868 100644 --- a/packages/server/api/src/app/ee/project-version/project-version.service.ts +++ b/packages/server/api/src/app/ee/project-release/project-release.service.ts @@ -1,23 +1,12 @@ -import { ActivepiecesError, ApId, apId, ErrorCode, FileId, ProjectId, ProjectVersion, SeekPage } from '@activepieces/shared' +import { ActivepiecesError, ApId, apId, ErrorCode, FileId, ProjectId, ProjectRelease, SeekPage } from '@activepieces/shared' import { Equal } from 'typeorm' import { repoFactory } from '../../core/db/repo-factory' -import { ProjectVersionEntity } from './project-version.entity' -export const projectVersionRepo = repoFactory(ProjectVersionEntity) +import { ProjectReleaseEntity } from './project-release.entity' +export const projectReleaseRepo = repoFactory(ProjectReleaseEntity) -export const projectVersionService = { - async create(params: CreateProjectVersionParams): Promise { - const projectVersionExists = await projectVersionRepo().findOne({ - where: { - fileId: Equal(params.fileId), - }, - }) - if (projectVersionExists) { - throw new ActivepiecesError({ - code: ErrorCode.ENTITY_NOT_FOUND, - params: { entityType: 'project_version', entityId: params.fileId, message: 'Project Version already exists' }, - }) - } +export const projectReleaseService = { + async create(params: CreateProjectReleaseParams): Promise { // TODO: uncomment this once we have a implement import project version // await fileService.getFileOrThrow({ // fileId: params.fileId, @@ -25,7 +14,7 @@ export const projectVersionService = { // type: FileType.PROJECT_VERSION // }) - return projectVersionRepo().save({ + return projectReleaseRepo().save({ id: apId(), created: new Date().toISOString(), updated: new Date().toISOString(), @@ -33,10 +22,12 @@ export const projectVersionService = { importedAt: new Date().toISOString(), importedBy: params.importedBy, fileId: params.fileId, + name: params.name, + description: params.description, }) }, - async list({ projectId }: ListParams): Promise> { - const projectVersions = await projectVersionRepo().find({ + async list({ projectId }: ListParams): Promise> { + const projectReleases = await projectReleaseRepo().find({ where: [ { projectId: Equal(projectId), @@ -48,17 +39,17 @@ export const projectVersionService = { }) return { - data: await Promise.all(projectVersions.map(async (projectVersion) => { + data: await Promise.all(projectReleases.map(async (projectRelease) => { return { - ...projectVersion, + ...projectRelease, } })), next: null, previous: null, } }, - async delete(params: DeleteProjectVersionParams): Promise { - await projectVersionRepo().delete({ + async delete(params: DeleteProjectReleaseParams): Promise { + await projectReleaseRepo().delete({ id: params.id, projectId: params.projectId, }) @@ -69,13 +60,15 @@ type ListParams = { projectId: ProjectId } -type CreateProjectVersionParams = { +type CreateProjectReleaseParams = { projectId: ProjectId fileId: FileId importedBy: ApId + name: string + description: string | null } -type DeleteProjectVersionParams = { +type DeleteProjectReleaseParams = { id: ApId projectId: ProjectId } \ No newline at end of file diff --git a/packages/server/api/src/app/ee/project-version/project-version.controller.ts b/packages/server/api/src/app/ee/project-version/project-version.controller.ts deleted file mode 100644 index 6b7e10ed14..0000000000 --- a/packages/server/api/src/app/ee/project-version/project-version.controller.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ApId, CreateProjectVersionRequestBody, ProjectVersion, SeekPage } from '@activepieces/shared' -import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' -import { StatusCodes } from 'http-status-codes' -import { projectVersionService } from './project-version.service' - -export const projectVersionController: FastifyPluginAsyncTypebox = async (app) => { - app.get('/', ListProjectVersionsRequest, async (req) => { - return projectVersionService.list({ - projectId: req.principal.projectId, - }) - }) - - app.post('/', CreateProjectVersionRequest, async (req, res) => { - const projectVersion = await projectVersionService.create({ - projectId: req.principal.projectId, - fileId: req.body.fileId, - importedBy: req.principal.id, - }) - return res.status(StatusCodes.CREATED).send(projectVersion) - // TODO: we should audit log this event - }) - - app.delete('/:id', DeleteProjectVersionRequest, async (req, res) => { - await projectVersionService.delete({ - id: req.params.id, - projectId: req.principal.projectId, - }) - return res.status(StatusCodes.NO_CONTENT).send() - }) - -} - -const ListProjectVersionsRequest = { - schema: { - response: { - [StatusCodes.OK]: SeekPage(ProjectVersion), - }, - }, -} - -const CreateProjectVersionRequest = { - schema: { - body: CreateProjectVersionRequestBody, - response: { - [StatusCodes.CREATED]: ProjectVersion, - }, - }, -} - -const DeleteProjectVersionRequest = { - schema: { - params: Type.Object({ - id: ApId, - }), - response: { - [StatusCodes.NO_CONTENT]: Type.Null(), - }, - }, -} \ No newline at end of file diff --git a/packages/server/api/src/app/ee/project-version/project-version.module.ts b/packages/server/api/src/app/ee/project-version/project-version.module.ts deleted file mode 100644 index b884dccf72..0000000000 --- a/packages/server/api/src/app/ee/project-version/project-version.module.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' -import { projectVersionController } from './project-version.controller' - -export const projectVersionModule: FastifyPluginAsyncTypebox = async (app) => { - await app.register(projectVersionController, { prefix: '/v1/project-versions' }) -} - \ No newline at end of file diff --git a/packages/server/api/test/helpers/mocks/index.ts b/packages/server/api/test/helpers/mocks/index.ts index ada2881c38..f0f928f3c0 100644 --- a/packages/server/api/test/helpers/mocks/index.ts +++ b/packages/server/api/test/helpers/mocks/index.ts @@ -40,7 +40,7 @@ import { Project, ProjectPlan, ProjectRole, - ProjectVersion, + ProjectRelease, RoleType, RunEnvironment, TemplateType, @@ -553,16 +553,16 @@ export const createMockProjectRole = (projectRole?: Partial): Proje } } -export const createMockProjectVersion = (projectVersion?: Partial): ProjectVersion => { +export const createMockProjectRelease = (projectRelease?: Partial): ProjectRelease => { return { - id: projectVersion?.id ?? apId(), - created: projectVersion?.created ?? faker.date.recent().toISOString(), - updated: projectVersion?.updated ?? faker.date.recent().toISOString(), - projectId: projectVersion?.projectId ?? apId(), - importedBy: projectVersion?.importedBy ?? apId(), - fileId: projectVersion?.fileId ?? apId(), - name: projectVersion?.name ?? faker.lorem.word(), - description: projectVersion?.description ?? faker.lorem.sentence(), + id: projectRelease?.id ?? apId(), + created: projectRelease?.created ?? faker.date.recent().toISOString(), + updated: projectRelease?.updated ?? faker.date.recent().toISOString(), + projectId: projectRelease?.projectId ?? apId(), + importedBy: projectRelease?.importedBy ?? apId(), + fileId: projectRelease?.fileId ?? apId(), + name: projectRelease?.name ?? faker.lorem.word(), + description: projectRelease?.description ?? faker.lorem.sentence(), } } diff --git a/packages/server/api/test/integration/cloud/project-version/project-version.test.ts b/packages/server/api/test/integration/cloud/project-release/project-release.test.ts similarity index 67% rename from packages/server/api/test/integration/cloud/project-version/project-version.test.ts rename to packages/server/api/test/integration/cloud/project-release/project-release.test.ts index 31c0b0d829..09530e1c2e 100644 --- a/packages/server/api/test/integration/cloud/project-version/project-version.test.ts +++ b/packages/server/api/test/integration/cloud/project-release/project-release.test.ts @@ -1,11 +1,11 @@ -import { PrincipalType, ProjectVersion } from '@activepieces/shared' +import { PrincipalType, ProjectRelease } from '@activepieces/shared' import { FastifyInstance } from 'fastify' import { StatusCodes } from 'http-status-codes' import { initializeDatabase } from '../../../../src/app/database' import { databaseConnection } from '../../../../src/app/database/database-connection' import { setupServer } from '../../../../src/app/server' import { generateMockToken } from '../../../helpers/auth' -import { createMockFile, createMockProjectVersion, mockBasicSetup } from '../../../helpers/mocks' +import { createMockFile, createMockProjectRelease, mockBasicSetup } from '../../../helpers/mocks' let app: FastifyInstance | null = null @@ -19,9 +19,9 @@ afterAll(async () => { await app?.close() }) -describe('Project Version API', () => { - describe('Create Project Version', () => { - it('should create a new project version', async () => { +describe('Project Release API', () => { + describe('Create Project Release', () => { + it('should create a new project release', async () => { const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockBasicSetup() const testToken = await generateMockToken({ type: PrincipalType.USER, @@ -33,14 +33,17 @@ describe('Project Version API', () => { const file = createMockFile({ platformId: mockPlatformOne.id, projectId: mockProjectOne.id }) await databaseConnection().getRepository('file').save(file) - const projectVersion = createMockProjectVersion({ projectId: mockProjectOne.id, fileId: file.id, importedBy: mockUserOne.id }) + const projectRelease = createMockProjectRelease({ projectId: mockProjectOne.id, fileId: file.id, importedBy: mockUserOne.id }) + await databaseConnection().getRepository('project_release').save(projectRelease) const response = await app?.inject({ method: 'POST', - url: '/v1/project-versions', + url: '/v1/project-releases', body: { fileId: file.id, importedBy: mockUserOne.id, + name: projectRelease.name, + description: projectRelease.description, }, headers: { authorization: `Bearer ${testToken}`, @@ -48,18 +51,18 @@ describe('Project Version API', () => { }) expect(response?.statusCode).toBe(StatusCodes.CREATED) - const responseBody = response?.json() as ProjectVersion + const responseBody = response?.json() as ProjectRelease expect(responseBody.id).toBeDefined() expect(responseBody.projectId).toBe(mockProjectOne.id) expect(responseBody.importedBy).toBe(mockUserOne.id) - expect(responseBody.fileId).toBe(projectVersion.fileId) - expect(responseBody.name).toBe(projectVersion.name) - expect(responseBody.description).toBe(projectVersion.description) + expect(responseBody.fileId).toBe(projectRelease.fileId) + expect(responseBody.name).toBe(projectRelease.name) + expect(responseBody.description).toBe(projectRelease.description) }) }) - describe('Delete Project Version', () => { - it('should delete a project version', async () => { + describe('Delete Project Release', () => { + it('should delete a project release', async () => { const { mockOwner: mockUserOne, mockPlatform: mockPlatformOne, mockProject: mockProjectOne } = await mockBasicSetup() const testToken = await generateMockToken({ type: PrincipalType.USER, @@ -71,20 +74,20 @@ describe('Project Version API', () => { const file = createMockFile({ platformId: mockPlatformOne.id, projectId: mockProjectOne.id }) await databaseConnection().getRepository('file').save(file) - const projectVersion = createMockProjectVersion({ projectId: mockProjectOne.id, fileId: file.id, importedBy: mockUserOne.id }) - await databaseConnection().getRepository('project_version').save(projectVersion) + const projectRelease = createMockProjectRelease({ projectId: mockProjectOne.id, fileId: file.id, importedBy: mockUserOne.id }) + await databaseConnection().getRepository('project_release').save(projectRelease) const response = await app?.inject({ method: 'DELETE', - url: `/v1/project-versions/${projectVersion.id}`, + url: `/v1/project-releases/${projectRelease.id}`, headers: { authorization: `Bearer ${testToken}`, }, }) expect(response?.statusCode).toBe(StatusCodes.NO_CONTENT) - const deletedProjectVersion = await databaseConnection().getRepository('project_version').findOne({ where: { id: projectVersion.id } }) - expect(deletedProjectVersion).toBeNull() + const deletedProjectRelease = await databaseConnection().getRepository('project_release').findOne({ where: { id: projectRelease.id } }) + expect(deletedProjectRelease).toBeNull() }) }) }) \ No newline at end of file diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d944c8fd25..7199caeb05 100755 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -73,8 +73,8 @@ export * from './lib/project-role/project-role' export * from './lib/project-role/project-role.request' export * from './lib/flows/operations/migrations' export * from './lib/flow-run/log-serializer' -export * from './lib/project-version/project-version' -export * from './lib/project-version/project-version.request' +export * from './lib/project-version/project-release' +export * from './lib/project-version/project-release.request' // Look at https://github.com/sinclairzx81/typebox/issues/350 import { TypeSystemPolicy } from '@sinclair/typebox/system' diff --git a/packages/shared/src/lib/project-version/project-release.request.ts b/packages/shared/src/lib/project-version/project-release.request.ts new file mode 100644 index 0000000000..21ef3c874c --- /dev/null +++ b/packages/shared/src/lib/project-version/project-release.request.ts @@ -0,0 +1,10 @@ +import { Static, Type } from '@sinclair/typebox' + +export const CreateProjectReleaseRequestBody = Type.Object({ + fileId: Type.String(), + importedBy: Type.String(), + name: Type.String(), + description: Type.Union([Type.String(), Type.Null()]), +}) + +export type CreateProjectReleaseRequestBody = Static \ No newline at end of file diff --git a/packages/shared/src/lib/project-version/project-version.ts b/packages/shared/src/lib/project-version/project-release.ts similarity index 61% rename from packages/shared/src/lib/project-version/project-version.ts rename to packages/shared/src/lib/project-version/project-release.ts index debbbd048f..3c70bb9a9b 100644 --- a/packages/shared/src/lib/project-version/project-version.ts +++ b/packages/shared/src/lib/project-version/project-release.ts @@ -1,13 +1,13 @@ import { Static, Type } from '@sinclair/typebox' import { BaseModelSchema } from '../common' -export const ProjectVersion = Type.Object({ +export const ProjectRelease = Type.Object({ ...BaseModelSchema, projectId: Type.String(), name: Type.String(), - description: Type.Optional(Type.String()), + description: Type.Union([Type.String(), Type.Null()]), importedBy: Type.Union([Type.String(), Type.Null()]), fileId: Type.String(), }) -export type ProjectVersion = Static +export type ProjectRelease = Static diff --git a/packages/shared/src/lib/project-version/project-version.request.ts b/packages/shared/src/lib/project-version/project-version.request.ts deleted file mode 100644 index 6c3e81396d..0000000000 --- a/packages/shared/src/lib/project-version/project-version.request.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Static, Type } from '@sinclair/typebox' - -export const CreateProjectVersionRequestBody = Type.Object({ - fileId: Type.String(), - importedBy: Type.String(), -}) - -export type CreateProjectVersionRequestBody = Static \ No newline at end of file From d25951f533ae53f36c0930b6602c9ac9fffd9b74 Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Tue, 17 Dec 2024 07:29:45 +0000 Subject: [PATCH 04/57] fix: remove importedBy from project release create request --- .../integration/cloud/project-release/project-release.test.ts | 1 - .../server/src/app/ee/projects/platform-project-service.ts | 1 - packages/shared/package.json | 4 ++-- .../shared/src/lib/project-version/project-release.request.ts | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 packages/server/src/app/ee/projects/platform-project-service.ts diff --git a/packages/server/api/test/integration/cloud/project-release/project-release.test.ts b/packages/server/api/test/integration/cloud/project-release/project-release.test.ts index 09530e1c2e..83cbf5ae22 100644 --- a/packages/server/api/test/integration/cloud/project-release/project-release.test.ts +++ b/packages/server/api/test/integration/cloud/project-release/project-release.test.ts @@ -41,7 +41,6 @@ describe('Project Release API', () => { url: '/v1/project-releases', body: { fileId: file.id, - importedBy: mockUserOne.id, name: projectRelease.name, description: projectRelease.description, }, diff --git a/packages/server/src/app/ee/projects/platform-project-service.ts b/packages/server/src/app/ee/projects/platform-project-service.ts deleted file mode 100644 index 0519ecba6e..0000000000 --- a/packages/server/src/app/ee/projects/platform-project-service.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json index 06ebcc6f1f..b1342253ff 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,5 +1,5 @@ { "name": "@activepieces/shared", - "version": "0.10.134", + "version": "0.10.135", "type": "commonjs" -} +} \ No newline at end of file diff --git a/packages/shared/src/lib/project-version/project-release.request.ts b/packages/shared/src/lib/project-version/project-release.request.ts index 21ef3c874c..ff30fdab06 100644 --- a/packages/shared/src/lib/project-version/project-release.request.ts +++ b/packages/shared/src/lib/project-version/project-release.request.ts @@ -2,7 +2,6 @@ import { Static, Type } from '@sinclair/typebox' export const CreateProjectReleaseRequestBody = Type.Object({ fileId: Type.String(), - importedBy: Type.String(), name: Type.String(), description: Type.Union([Type.String(), Type.Null()]), }) From 879919d394cfbbef1911f2c4aed22821daedd895 Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Tue, 17 Dec 2024 08:22:32 +0000 Subject: [PATCH 05/57] fix: lint errors and change releases icon from box to package --- .../app/components/dashboard-container.tsx | 9 +------- packages/server/api/src/app/app.ts | 2 +- .../src/app/database/database-connection.ts | 2 +- ...1734418823028-CreateProjectReleaseTable.ts | 22 +++++++++---------- .../src/app/database/postgres-connection.ts | 2 +- .../project-release.controller.ts | 11 +++++++++- .../project-release.service.ts | 2 +- .../server/api/test/helpers/mocks/index.ts | 2 +- .../project-members/project-members.test.ts | 6 ++--- .../project/platform-project-service.test.ts | 4 ++-- .../integration/cloud/project/project.test.ts | 2 +- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/react-ui/src/app/components/dashboard-container.tsx b/packages/react-ui/src/app/components/dashboard-container.tsx index 373fbac067..27913c5e14 100644 --- a/packages/react-ui/src/app/components/dashboard-container.tsx +++ b/packages/react-ui/src/app/components/dashboard-container.tsx @@ -1,12 +1,5 @@ import { t } from 'i18next'; -import { - AlertCircle, - Box, - Link2, - Logs, - Workflow, - Wrench, -} from 'lucide-react'; +import { AlertCircle, Box, Link2, Logs, Workflow, Wrench } from 'lucide-react'; import { Navigate } from 'react-router-dom'; import { useEmbedding } from '@/components/embed-provider'; diff --git a/packages/server/api/src/app/app.ts b/packages/server/api/src/app/app.ts index 7d0f6d1c01..d39ea3495b 100644 --- a/packages/server/api/src/app/app.ts +++ b/packages/server/api/src/app/app.ts @@ -52,8 +52,8 @@ import { enterprisePieceMetadataServiceHooks } from './ee/pieces/filters/enterpr import { platformPieceModule } from './ee/pieces/platform-piece-module' import { adminPlatformPieceModule } from './ee/platform/admin-platform.controller' import { projectMemberModule } from './ee/project-members/project-member.module' -import { projectRoleModule } from './ee/project-role/project-role.module' import { projectReleaseModule } from './ee/project-release/project-release.module' +import { projectRoleModule } from './ee/project-role/project-role.module' import { projectEnterpriseHooks } from './ee/projects/ee-project-hooks' import { platformProjectModule } from './ee/projects/platform-project-module' import { signingKeyModule } from './ee/signing-key/signing-key-module' diff --git a/packages/server/api/src/app/database/database-connection.ts b/packages/server/api/src/app/database/database-connection.ts index 5431e20efb..e03300b500 100644 --- a/packages/server/api/src/app/database/database-connection.ts +++ b/packages/server/api/src/app/database/database-connection.ts @@ -25,8 +25,8 @@ import { OAuthAppEntity } from '../ee/oauth-apps/oauth-app.entity' import { OtpEntity } from '../ee/otp/otp-entity' import { ProjectMemberEntity } from '../ee/project-members/project-member.entity' import { ProjectPlanEntity } from '../ee/project-plan/project-plan.entity' -import { ProjectRoleEntity } from '../ee/project-role/project-role.entity' import { ProjectReleaseEntity } from '../ee/project-release/project-release.entity' +import { ProjectRoleEntity } from '../ee/project-role/project-role.entity' import { SigningKeyEntity } from '../ee/signing-key/signing-key-entity' import { FileEntity } from '../file/file.entity' import { FlagEntity } from '../flags/flag.entity' diff --git a/packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts b/packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts index b233d5fec6..9c08abf6c0 100644 --- a/packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts +++ b/packages/server/api/src/app/database/migration/postgres/1734418823028-CreateProjectReleaseTable.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm' export class CreateProjectReleaseTable1734418823028 implements MigrationInterface { name = 'CreateProjectReleaseTable1734418823028' @@ -16,41 +16,41 @@ export class CreateProjectReleaseTable1734418823028 implements MigrationInterfac "fileId" character varying NOT NULL, CONSTRAINT "PK_11aa4566a8a7a623e5c3f9809fe" PRIMARY KEY ("id") ) - `); + `) await queryRunner.query(` CREATE INDEX "idx_project_release_project_id" ON "project_release" ("projectId") - `); + `) await queryRunner.query(` ALTER TABLE "project_release" ADD CONSTRAINT "fk_project_release_project_id" FOREIGN KEY ("projectId") REFERENCES "project"("id") ON DELETE CASCADE ON UPDATE NO ACTION - `); + `) await queryRunner.query(` ALTER TABLE "project_release" ADD CONSTRAINT "fk_project_release_imported_by" FOREIGN KEY ("importedBy") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION - `); + `) await queryRunner.query(` ALTER TABLE "project_release" ADD CONSTRAINT "fk_project_release_file_id" FOREIGN KEY ("fileId") REFERENCES "file"("id") ON DELETE CASCADE ON UPDATE NO ACTION - `); + `) } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "project_release" DROP CONSTRAINT "fk_project_release_file_id" - `); + `) await queryRunner.query(` ALTER TABLE "project_release" DROP CONSTRAINT "fk_project_release_imported_by" - `); + `) await queryRunner.query(` ALTER TABLE "project_release" DROP CONSTRAINT "fk_project_release_project_id" - `); + `) await queryRunner.query(` DROP INDEX "public"."idx_project_release_project_id" - `); + `) await queryRunner.query(` DROP TABLE "project_release" - `); + `) } } diff --git a/packages/server/api/src/app/database/postgres-connection.ts b/packages/server/api/src/app/database/postgres-connection.ts index 5b2b012a99..6d1fc749c5 100644 --- a/packages/server/api/src/app/database/postgres-connection.ts +++ b/packages/server/api/src/app/database/postgres-connection.ts @@ -277,7 +277,7 @@ const getMigrations = (): (new () => MigrationInterface)[] => { AddGlobalConnectionsAndRbacForPlatform1731532843905, AddIndiciesToRunAndTriggerData1732324567513, AddProjectRelationInUserInvitation1732790412900, - CreateProjectReleaseTable1734418823028 + CreateProjectReleaseTable1734418823028, ] const edition = system.getEdition() diff --git a/packages/server/api/src/app/ee/project-release/project-release.controller.ts b/packages/server/api/src/app/ee/project-release/project-release.controller.ts index 9b4648753b..d41a1234cf 100644 --- a/packages/server/api/src/app/ee/project-release/project-release.controller.ts +++ b/packages/server/api/src/app/ee/project-release/project-release.controller.ts @@ -1,4 +1,4 @@ -import { ApId, CreateProjectReleaseRequestBody, ProjectRelease, SeekPage } from '@activepieces/shared' +import { ApId, CreateProjectReleaseRequestBody, PrincipalType, ProjectRelease, SeekPage } from '@activepieces/shared' import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' import { StatusCodes } from 'http-status-codes' import { projectReleaseService } from './project-release.service' @@ -33,6 +33,9 @@ export const projectReleaseController: FastifyPluginAsyncTypebox = async (app) = } const ListProjectReleasesRequest = { + config: { + allowedPrincipals: [PrincipalType.USER], + }, schema: { response: { [StatusCodes.OK]: SeekPage(ProjectRelease), @@ -41,6 +44,9 @@ const ListProjectReleasesRequest = { } const CreateProjectReleaseRequest = { + config: { + allowedPrincipals: [PrincipalType.USER], + }, schema: { body: CreateProjectReleaseRequestBody, response: { @@ -50,6 +56,9 @@ const CreateProjectReleaseRequest = { } const DeleteProjectReleaseRequest = { + config: { + allowedPrincipals: [PrincipalType.USER], + }, schema: { params: Type.Object({ id: ApId, diff --git a/packages/server/api/src/app/ee/project-release/project-release.service.ts b/packages/server/api/src/app/ee/project-release/project-release.service.ts index bc1d4e1868..1ff34b2970 100644 --- a/packages/server/api/src/app/ee/project-release/project-release.service.ts +++ b/packages/server/api/src/app/ee/project-release/project-release.service.ts @@ -1,4 +1,4 @@ -import { ActivepiecesError, ApId, apId, ErrorCode, FileId, ProjectId, ProjectRelease, SeekPage } from '@activepieces/shared' +import { ApId, apId, FileId, ProjectId, ProjectRelease, SeekPage } from '@activepieces/shared' import { Equal } from 'typeorm' import { repoFactory } from '../../core/db/repo-factory' import { ProjectReleaseEntity } from './project-release.entity' diff --git a/packages/server/api/test/helpers/mocks/index.ts b/packages/server/api/test/helpers/mocks/index.ts index f0f928f3c0..6c0c613fad 100644 --- a/packages/server/api/test/helpers/mocks/index.ts +++ b/packages/server/api/test/helpers/mocks/index.ts @@ -39,8 +39,8 @@ import { PlatformRole, Project, ProjectPlan, - ProjectRole, ProjectRelease, + ProjectRole, RoleType, RunEnvironment, TemplateType, diff --git a/packages/server/api/test/integration/cloud/project-members/project-members.test.ts b/packages/server/api/test/integration/cloud/project-members/project-members.test.ts index 03989177df..ecd70828c4 100644 --- a/packages/server/api/test/integration/cloud/project-members/project-members.test.ts +++ b/packages/server/api/test/integration/cloud/project-members/project-members.test.ts @@ -29,10 +29,8 @@ beforeAll(async () => { }) beforeEach(async () => { - (stripeHelper as any).getOrCreateCustomer = jest - .fn() - .mockResolvedValue(faker.string.uuid()) - (emailService).sendInvitation = jest.fn() + stripeHelper.getOrCreateCustomer = jest.fn().mockResolvedValue(faker.string.uuid()) + emailService.sendInvitation = jest.fn() }) afterAll(async () => { diff --git a/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts b/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts index c415154215..5cca7ec3ec 100644 --- a/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts +++ b/packages/server/api/test/integration/cloud/project/platform-project-service.test.ts @@ -43,7 +43,7 @@ describe('Platform Project Service', () => { // act if (!app) { - throw new Error("Fastify instance is not initialized"); + throw new Error('Fastify instance is not initialized') } await platformProjectService(app.log).hardDelete({ id: mockProject.id }) @@ -70,7 +70,7 @@ describe('Platform Project Service', () => { // act if (!app) { - throw new Error("Fastify instance is not initialized"); + throw new Error('Fastify instance is not initialized') } await platformProjectService(app.log).hardDelete({ id: mockProject.id }) diff --git a/packages/server/api/test/integration/cloud/project/project.test.ts b/packages/server/api/test/integration/cloud/project/project.test.ts index 76df246d4a..afc5578423 100644 --- a/packages/server/api/test/integration/cloud/project/project.test.ts +++ b/packages/server/api/test/integration/cloud/project/project.test.ts @@ -41,7 +41,7 @@ afterAll(async () => { await app?.close() }) beforeEach(async () => { - (stripeHelper as any).getOrCreateCustomer = jest + stripeHelper.getOrCreateCustomer = jest .fn() .mockResolvedValue(faker.string.uuid()) }) From ee087cf40f65316eb93fabc1a268e71480fd64d4 Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Tue, 17 Dec 2024 12:49:34 +0000 Subject: [PATCH 06/57] feat: add configure releases --- .../src/lib/project/project-requests.ts | 1 + .../app/components/dashboard-container.tsx | 3 ++ .../src/app/components/flow-actions-menu.tsx | 2 +- .../components/project-settings-layout.tsx | 4 +- .../src/app/components/request-trial.tsx | 2 +- packages/react-ui/src/app/router/index.tsx | 8 +-- .../react-ui/src/app/routes/flows/index.tsx | 2 +- .../platform/setup/license-key/index.tsx | 2 +- .../{git-sync => environment}/index.tsx | 17 +++--- .../settings/environment/release-card.tsx | 54 +++++++++++++++++++ .../components/connect-git-dialog.tsx | 2 +- .../components/push-to-git-dialog.tsx | 2 +- ...734429537542-AddReleasesEnablingBoolean.ts | 27 ++++++++++ ...734431436773-RenameGitSyncToEnvironment.ts | 28 ++++++++++ .../src/app/database/postgres-connection.ts | 4 ++ .../src/app/ee/git-sync/git-sync.module.ts | 2 +- .../ee/license-keys/license-keys-service.ts | 5 +- .../api/src/app/platform/platform.entity.ts | 2 +- .../api/src/app/platform/platform.service.ts | 6 +-- .../api/src/app/project/project-entity.ts | 5 ++ .../api/src/app/project/project-service.ts | 3 ++ .../server/api/test/helpers/mocks/index.ts | 3 +- .../cloud/git-repos/git-repos.test.ts | 2 +- packages/shared/src/lib/license-keys/index.ts | 2 +- .../shared/src/lib/platform/platform.model.ts | 2 +- packages/shared/src/lib/project/project.ts | 1 + 26 files changed, 161 insertions(+), 30 deletions(-) rename packages/react-ui/src/app/routes/settings/{git-sync => environment}/index.tsx (90%) create mode 100644 packages/react-ui/src/app/routes/settings/environment/release-card.tsx create mode 100644 packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts create mode 100644 packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts diff --git a/packages/ee/shared/src/lib/project/project-requests.ts b/packages/ee/shared/src/lib/project/project-requests.ts index c455f3c396..e9303b792b 100644 --- a/packages/ee/shared/src/lib/project/project-requests.ts +++ b/packages/ee/shared/src/lib/project/project-requests.ts @@ -3,6 +3,7 @@ import { NotificationStatus, PiecesFilterType, SAFE_STRING_PATTERN } from "@acti export const UpdateProjectPlatformRequest = Type.Object({ notifyStatus: Type.Optional(Type.Enum(NotificationStatus)), + releasesEnabled: Type.Optional(Type.Boolean()), displayName: Type.Optional(Type.String({ pattern: SAFE_STRING_PATTERN, })), diff --git a/packages/react-ui/src/app/components/dashboard-container.tsx b/packages/react-ui/src/app/components/dashboard-container.tsx index 27913c5e14..a1efc00888 100644 --- a/packages/react-ui/src/app/components/dashboard-container.tsx +++ b/packages/react-ui/src/app/components/dashboard-container.tsx @@ -12,6 +12,7 @@ import { authenticationSession } from '../../lib/authentication-session'; import { AllowOnlyLoggedInUserOnlyGuard } from './allow-logged-in-user-only-guard'; import { Sidebar, SidebarLink } from './sidebar'; +import { projectHooks } from '@/hooks/project-hooks'; type DashboardContainerProps = { children: React.ReactNode; @@ -22,6 +23,7 @@ export function DashboardContainer({ children }: DashboardContainerProps) { const { data: showIssuesNotification } = issueHooks.useIssuesNotification( platform.flowIssuesEnabled, ); + const { project } = projectHooks.useCurrentProject() const { embedState } = useEmbedding(); const currentProjectId = authenticationSession.getProjectId(); @@ -68,6 +70,7 @@ export function DashboardContainer({ children }: DashboardContainerProps) { to: '/releases', label: t('Releases'), icon: Box, + hasPermission: project.releasesEnabled, }, { to: '/settings', diff --git a/packages/react-ui/src/app/components/flow-actions-menu.tsx b/packages/react-ui/src/app/components/flow-actions-menu.tsx index f15a65ad26..482f9a3f7c 100644 --- a/packages/react-ui/src/app/components/flow-actions-menu.tsx +++ b/packages/react-ui/src/app/components/flow-actions-menu.tsx @@ -73,7 +73,7 @@ const FlowActionMenu: React.FC = ({ const openNewWindow = useNewWindow(); const { gitSync } = gitSyncHooks.useGitSync( authenticationSession.getProjectId()!, - platform.gitSyncEnabled, + platform.environmentEnabled, ); const { checkAccess } = useAuthorization(); const userHasPermissionToUpdateFlow = checkAccess(Permission.WRITE_FLOW); diff --git a/packages/react-ui/src/app/components/project-settings-layout.tsx b/packages/react-ui/src/app/components/project-settings-layout.tsx index 19c0d806f3..43a86c1690 100644 --- a/packages/react-ui/src/app/components/project-settings-layout.tsx +++ b/packages/react-ui/src/app/components/project-settings-layout.tsx @@ -60,8 +60,8 @@ export default function ProjectSettingsLayout({ hasPermission: checkAccess(Permission.READ_ALERT), }, { - title: t('Git Sync'), - href: '/settings/git-sync', + title: t('Environment'), + href: '/settings/environment', icon: , hasPermission: checkAccess(Permission.READ_GIT_REPO), }, diff --git a/packages/react-ui/src/app/components/request-trial.tsx b/packages/react-ui/src/app/components/request-trial.tsx index d24297d5a7..58e1cc06ae 100644 --- a/packages/react-ui/src/app/components/request-trial.tsx +++ b/packages/react-ui/src/app/components/request-trial.tsx @@ -60,7 +60,7 @@ export type FeatureKey = | 'API' | 'SSO' | 'AUDIT_LOGS' - | 'GIT_SYNC' + | 'ENVIRONMENT' | 'ISSUES' | 'ANALYTICS' | 'ALERTS' diff --git a/packages/react-ui/src/app/router/index.tsx b/packages/react-ui/src/app/router/index.tsx index c80a4d53e4..68b75bd6c5 100644 --- a/packages/react-ui/src/app/router/index.tsx +++ b/packages/react-ui/src/app/router/index.tsx @@ -61,7 +61,6 @@ import { FlowRunPage } from '../routes/runs/id'; import AlertsPage from '../routes/settings/alerts'; import AppearancePage from '../routes/settings/appearance'; import GeneralPage from '../routes/settings/general'; -import { GitSyncPage } from '../routes/settings/git-sync'; import TeamPage from '../routes/settings/team'; import { SignInPage } from '../routes/sign-in'; import { SignUpPage } from '../routes/sign-up'; @@ -72,6 +71,7 @@ import { DefaultRoute } from './default-route'; import { FlagRouteGuard } from './flag-route-guard'; import { RoutePermissionGuard } from './permission-guard'; import { ProjectRouterWrapper } from './project-route-wrapper'; +import { EnvironmentPage } from '../routes/settings/environment'; const SettingsRerouter = () => { const { hash } = useLocation(); @@ -342,13 +342,13 @@ const routes = [ }, ...ProjectRouterWrapper({ - path: '/settings/git-sync', + path: '/settings/environment', element: ( - - + + diff --git a/packages/react-ui/src/app/routes/flows/index.tsx b/packages/react-ui/src/app/routes/flows/index.tsx index b4ef5e9b75..09b259d13d 100644 --- a/packages/react-ui/src/app/routes/flows/index.tsx +++ b/packages/react-ui/src/app/routes/flows/index.tsx @@ -95,7 +95,7 @@ const FlowsPage = () => { const { platform } = platformHooks.useCurrentPlatform(); const { gitSync } = gitSyncHooks.useGitSync( authenticationSession.getProjectId()!, - platform.gitSyncEnabled, + platform.environmentEnabled, ); const userHasPermissionToUpdateFlow = checkAccess(Permission.WRITE_FLOW); const userHasPermissionToPushToGit = checkAccess(Permission.WRITE_GIT_REPO); diff --git a/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx b/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx index 2a5b51e096..007db4f4d3 100644 --- a/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx +++ b/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx @@ -34,7 +34,7 @@ import { ApEdition, ApFlagId, isNil } from '@activepieces/shared'; import { ActivateLicenseDialog } from './activate-license-dialog'; const LICENSE_PROPS_MAP = { - gitSyncEnabled: 'Team Collaboration via Git', + environmentEnabled: 'Team Collaboration via Git', analyticsEnabled: 'Analytics', auditLogEnabled: 'Audit Log', embeddingEnabled: 'Embedding', diff --git a/packages/react-ui/src/app/routes/settings/git-sync/index.tsx b/packages/react-ui/src/app/routes/settings/environment/index.tsx similarity index 90% rename from packages/react-ui/src/app/routes/settings/git-sync/index.tsx rename to packages/react-ui/src/app/routes/settings/environment/index.tsx index d458115ce0..531163da2f 100644 --- a/packages/react-ui/src/app/routes/settings/git-sync/index.tsx +++ b/packages/react-ui/src/app/routes/settings/environment/index.tsx @@ -1,5 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { t } from 'i18next'; +import { useState } from 'react'; import LockedFeatureGuard from '@/app/components/locked-feature-guard'; import { Button } from '@/components/ui/button'; @@ -13,13 +14,14 @@ import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks'; import { platformHooks } from '@/hooks/platform-hooks'; import { authenticationSession } from '@/lib/authentication-session'; import { assertNotNullOrUndefined } from '@activepieces/shared'; +import { ReleaseCard } from './release-card'; -const GitSyncPage = () => { +const EnvironmentPage = () => { const { platform } = platformHooks.useCurrentPlatform(); const { gitSync, isLoading, refetch } = gitSyncHooks.useGitSync( authenticationSession.getProjectId()!, - platform.gitSyncEnabled, + platform.environmentEnabled, ); const { mutate } = useMutation({ @@ -42,16 +44,16 @@ const GitSyncPage = () => { return (
-

{t('Git Sync')}

+

{t('Environment')}

{t( 'This feature allows for the creation of an external backup, environments, and maintaining a version history', @@ -101,9 +103,10 @@ const GitSyncPage = () => { )}
+
); }; -export { GitSyncPage }; +export { EnvironmentPage }; diff --git a/packages/react-ui/src/app/routes/settings/environment/release-card.tsx b/packages/react-ui/src/app/routes/settings/environment/release-card.tsx new file mode 100644 index 0000000000..edc9ff88ba --- /dev/null +++ b/packages/react-ui/src/app/routes/settings/environment/release-card.tsx @@ -0,0 +1,54 @@ +import { t } from 'i18next'; +import { Package } from 'lucide-react'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { toast } from '@/components/ui/use-toast'; +import { INTERNAL_ERROR_TOAST } from '@/components/ui/use-toast'; +import { useMutation } from '@tanstack/react-query'; +import { projectHooks } from '@/hooks/project-hooks'; +import { projectApi } from '@/lib/project-api'; + +const ReleaseCard = () => { + const { project, refetch } = projectHooks.useCurrentProject() + + const { mutate } = useMutation({ + mutationFn: () => { + return projectApi.update(project.id, { + releasesEnabled: !project.releasesEnabled, + }) + }, + onSuccess: () => { + refetch() + toast({ + title: t('Releases Enabled'), + description: t('You have successfully enabled releases'), + duration: 3000, + }); + }, + onError: () => { + toast(INTERNAL_ERROR_TOAST); + }, + }); + + return ( + +
+
+ +
+
+
{t('Releases')}
+
+ {t('Enable releases to easily create and manage project releases.')} +
+
+
+ +
+
+
+ ); +}; + +ReleaseCard.displayName = 'GitSyncCard'; +export { ReleaseCard }; diff --git a/packages/react-ui/src/features/git-sync/components/connect-git-dialog.tsx b/packages/react-ui/src/features/git-sync/components/connect-git-dialog.tsx index ac1765630b..9c63364a4d 100644 --- a/packages/react-ui/src/features/git-sync/components/connect-git-dialog.tsx +++ b/packages/react-ui/src/features/git-sync/components/connect-git-dialog.tsx @@ -56,7 +56,7 @@ const ConnectGitDialog = () => { const { refetch } = gitSyncHooks.useGitSync( projectId, - platform.gitSyncEnabled, + platform.environmentEnabled, ); const { mutate, isPending } = useMutation({ diff --git a/packages/react-ui/src/features/git-sync/components/push-to-git-dialog.tsx b/packages/react-ui/src/features/git-sync/components/push-to-git-dialog.tsx index c0e37367fc..f2e590db9c 100644 --- a/packages/react-ui/src/features/git-sync/components/push-to-git-dialog.tsx +++ b/packages/react-ui/src/features/git-sync/components/push-to-git-dialog.tsx @@ -46,7 +46,7 @@ const PushToGitDialog = ({ children, flowIds }: PushToGitDialogProps) => { const { platform } = platformHooks.useCurrentPlatform(); const { gitSync } = gitSyncHooks.useGitSync( authenticationSession.getProjectId()!, - platform.gitSyncEnabled, + platform.environmentEnabled, ); const form = useForm({ diff --git a/packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts b/packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts new file mode 100644 index 0000000000..9d71930d4c --- /dev/null +++ b/packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { isNotOneOfTheseEditions } from "../../database-common"; +import { ApEdition } from "@activepieces/shared"; + +export class AddReleasesEnablingBoolean1734429537542 implements MigrationInterface { + name = 'AddReleasesEnablingBoolean1734429537542' + + public async up(queryRunner: QueryRunner): Promise { + if (isNotOneOfTheseEditions([ApEdition.CLOUD, ApEdition.ENTERPRISE])) { + return + } + await queryRunner.query(` + ALTER TABLE "project" + ADD "releasesEnabled" boolean NOT NULL DEFAULT false + `); + } + + public async down(queryRunner: QueryRunner): Promise { + if (isNotOneOfTheseEditions([ApEdition.CLOUD, ApEdition.ENTERPRISE])) { + return + } + await queryRunner.query(` + ALTER TABLE "project" DROP COLUMN "releasesEnabled" + `); + } + +} diff --git a/packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts b/packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts new file mode 100644 index 0000000000..0f6c12e764 --- /dev/null +++ b/packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts @@ -0,0 +1,28 @@ +import { ApEdition } from "@activepieces/shared"; +import { MigrationInterface, QueryRunner } from "typeorm"; +import { isNotOneOfTheseEditions } from "../../database-common"; + +export class RenameGitSyncToEnvironment1734431436773 implements MigrationInterface { + name = 'RenameGitSyncToEnvironment1734431436773' + + public async up(queryRunner: QueryRunner): Promise { + if (isNotOneOfTheseEditions([ApEdition.CLOUD, ApEdition.ENTERPRISE])) { + return + } + await queryRunner.query(` + ALTER TABLE "platform" + RENAME COLUMN "gitSyncEnabled" TO "environmentEnabled" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + if (isNotOneOfTheseEditions([ApEdition.CLOUD, ApEdition.ENTERPRISE])) { + return + } + await queryRunner.query(` + ALTER TABLE "platform" + RENAME COLUMN "environmentEnabled" TO "gitSyncEnabled" + `); + } + +} diff --git a/packages/server/api/src/app/database/postgres-connection.ts b/packages/server/api/src/app/database/postgres-connection.ts index 6d1fc749c5..3e06f2b451 100644 --- a/packages/server/api/src/app/database/postgres-connection.ts +++ b/packages/server/api/src/app/database/postgres-connection.ts @@ -166,6 +166,8 @@ import { AddAuditLogIndicies1731711188507 } from './migration/postgres/173171118 import { AddIndiciesToRunAndTriggerData1732324567513 } from './migration/postgres/1732324567513-AddIndiciesToRunAndTriggerData' import { AddProjectRelationInUserInvitation1732790412900 } from './migration/postgres/1732790673766-AddProjectRelationInUserInvitation' import { CreateProjectReleaseTable1734418823028 } from './migration/postgres/1734418823028-CreateProjectReleaseTable' +import { AddReleasesEnablingBoolean1734429537542 } from './migration/postgres/1734429537542-AddReleasesEnablingBoolean' +import { RenameGitSyncToEnvironment1734431436773 } from './migration/postgres/1734431436773-RenameGitSyncToEnvironment' const getSslConfig = (): boolean | TlsOptions => { const useSsl = system.get(AppSystemProp.POSTGRES_USE_SSL) @@ -278,6 +280,8 @@ const getMigrations = (): (new () => MigrationInterface)[] => { AddIndiciesToRunAndTriggerData1732324567513, AddProjectRelationInUserInvitation1732790412900, CreateProjectReleaseTable1734418823028, + AddReleasesEnablingBoolean1734429537542, + RenameGitSyncToEnvironment1734431436773, ] const edition = system.getEdition() diff --git a/packages/server/api/src/app/ee/git-sync/git-sync.module.ts b/packages/server/api/src/app/ee/git-sync/git-sync.module.ts index 4a0b981ef0..3f61ced28c 100644 --- a/packages/server/api/src/app/ee/git-sync/git-sync.module.ts +++ b/packages/server/api/src/app/ee/git-sync/git-sync.module.ts @@ -20,7 +20,7 @@ import { gitRepoService } from './git-sync.service' export const gitRepoModule: FastifyPluginAsync = async (app) => { app.addHook('preSerialization', entitiesMustBeOwnedByCurrentProject) - app.addHook('preHandler', platformMustHaveFeatureEnabled((platform) => platform.gitSyncEnabled)) + app.addHook('preHandler', platformMustHaveFeatureEnabled((platform) => platform.environmentEnabled)) await app.register(gitRepoController, { prefix: '/v1/git-repos' }) } diff --git a/packages/server/api/src/app/ee/license-keys/license-keys-service.ts b/packages/server/api/src/app/ee/license-keys/license-keys-service.ts index ccc8c205d1..8ba1c07fe6 100644 --- a/packages/server/api/src/app/ee/license-keys/license-keys-service.ts +++ b/packages/server/api/src/app/ee/license-keys/license-keys-service.ts @@ -103,7 +103,8 @@ export const licenseKeysService = (log: FastifyBaseLogger) => ({ await platformService.update({ id: platformId, ssoEnabled: key.ssoEnabled, - gitSyncEnabled: key.gitSyncEnabled, + // TODO: ADD THIS COLUMN TO SUPABASE + environmentEnabled: true, showPoweredBy: key.showPoweredBy, embeddingEnabled: key.embeddingEnabled, auditLogEnabled: key.auditLogEnabled, @@ -161,7 +162,7 @@ const deletePrivatePieces = async (platformId: string, log: FastifyBaseLogger): const turnedOffFeatures: Omit = { ssoEnabled: false, analyticsEnabled: false, - gitSyncEnabled: false, + environmentEnabled: false, showPoweredBy: false, embeddingEnabled: false, auditLogEnabled: false, diff --git a/packages/server/api/src/app/platform/platform.entity.ts b/packages/server/api/src/app/platform/platform.entity.ts index a39cd30537..86175a64df 100644 --- a/packages/server/api/src/app/platform/platform.entity.ts +++ b/packages/server/api/src/app/platform/platform.entity.ts @@ -104,7 +104,7 @@ export const PlatformEntity = new EntitySchema({ enum: FilteredPieceBehavior, nullable: false, }, - gitSyncEnabled: { + environmentEnabled: { type: Boolean, nullable: false, }, diff --git a/packages/server/api/src/app/platform/platform.service.ts b/packages/server/api/src/app/platform/platform.service.ts index 4c32b2f792..83cb7b1056 100644 --- a/packages/server/api/src/app/platform/platform.service.ts +++ b/packages/server/api/src/app/platform/platform.service.ts @@ -57,7 +57,7 @@ export const platformService = { federatedAuthProviders: {}, cloudAuthEnabled: true, flowIssuesEnabled: false, - gitSyncEnabled: false, + environmentEnabled: false, managePiecesEnabled: false, manageTemplatesEnabled: false, manageProjectsEnabled: false, @@ -111,7 +111,7 @@ export const platformService = { ...spreadIfDefined('cloudAuthEnabled', params.cloudAuthEnabled), ...spreadIfDefined('defaultLocale', params.defaultLocale), ...spreadIfDefined('showPoweredBy', params.showPoweredBy), - ...spreadIfDefined('gitSyncEnabled', params.gitSyncEnabled), + ...spreadIfDefined('environmentEnabled', params.environmentEnabled), ...spreadIfDefined('embeddingEnabled', params.embeddingEnabled), ...spreadIfDefined('globalConnectionsEnabled', params.globalConnectionsEnabled), ...spreadIfDefined('customRolesEnabled', params.customRolesEnabled), @@ -182,7 +182,7 @@ type UpdateParams = UpdatePlatformRequestBody & { auditLogEnabled?: boolean showPoweredBy?: boolean ssoEnabled?: boolean - gitSyncEnabled?: boolean + environmentEnabled?: boolean embeddingEnabled?: boolean globalConnectionsEnabled?: boolean customRolesEnabled?: boolean diff --git a/packages/server/api/src/app/project/project-entity.ts b/packages/server/api/src/app/project/project-entity.ts index f613c59d76..410652dad5 100644 --- a/packages/server/api/src/app/project/project-entity.ts +++ b/packages/server/api/src/app/project/project-entity.ts @@ -43,6 +43,11 @@ export const ProjectEntity = new EntitySchema({ type: String, nullable: true, }, + releasesEnabled: { + type: Boolean, + nullable: false, + default: false, + }, }, indices: [ { diff --git a/packages/server/api/src/app/project/project-service.ts b/packages/server/api/src/app/project/project-service.ts index 858a7f0d54..b9df330eae 100644 --- a/packages/server/api/src/app/project/project-service.ts +++ b/packages/server/api/src/app/project/project-service.ts @@ -24,6 +24,7 @@ export const projectService = { id: apId(), ...params, notifyStatus: NotificationStatus.ALWAYS, + releasesEnabled: false, } const savedProject = await projectRepo().save(newProject) await projectHooks.get(logger).postCreate(savedProject) @@ -50,6 +51,7 @@ export const projectService = { { ...spreadIfDefined('displayName', request.displayName), ...spreadIfDefined('notifyStatus', request.notifyStatus), + ...spreadIfDefined('releasesEnabled', request.releasesEnabled), }, ) return this.getOneOrThrow(projectId) @@ -145,6 +147,7 @@ export const projectService = { type UpdateParams = { displayName?: string notifyStatus?: NotificationStatus + releasesEnabled?: boolean } type CreateParams = { diff --git a/packages/server/api/test/helpers/mocks/index.ts b/packages/server/api/test/helpers/mocks/index.ts index 6c0c613fad..f0f667a457 100644 --- a/packages/server/api/test/helpers/mocks/index.ts +++ b/packages/server/api/test/helpers/mocks/index.ts @@ -161,6 +161,7 @@ export const createMockProject = (project?: Partial): Project => { project?.notifyStatus ?? faker.helpers.enumValue(NotificationStatus), platformId: project?.platformId ?? apId(), externalId: project?.externalId ?? apId(), + releasesEnabled: project?.releasesEnabled ?? false, } } @@ -206,7 +207,7 @@ export const createMockPlatform = (platform?: Partial): Platform => { faker.helpers.enumValue(FilteredPieceBehavior), smtp: platform?.smtp, flowIssuesEnabled: platform?.flowIssuesEnabled ?? faker.datatype.boolean(), - gitSyncEnabled: platform?.gitSyncEnabled ?? faker.datatype.boolean(), + environmentEnabled: platform?.environmentEnabled ?? faker.datatype.boolean(), embeddingEnabled: platform?.embeddingEnabled ?? faker.datatype.boolean(), cloudAuthEnabled: platform?.cloudAuthEnabled ?? faker.datatype.boolean(), showPoweredBy: platform?.showPoweredBy ?? faker.datatype.boolean(), diff --git a/packages/server/api/test/integration/cloud/git-repos/git-repos.test.ts b/packages/server/api/test/integration/cloud/git-repos/git-repos.test.ts index 991aa4d209..8106e71cf3 100644 --- a/packages/server/api/test/integration/cloud/git-repos/git-repos.test.ts +++ b/packages/server/api/test/integration/cloud/git-repos/git-repos.test.ts @@ -269,7 +269,7 @@ describe('Git API', () => { const mockEnvironment = async () => { const { mockPlatform, mockOwner, mockProject } = await mockBasicSetup({ platform: { - gitSyncEnabled: true, + environmentEnabled: true, }, }) diff --git a/packages/shared/src/lib/license-keys/index.ts b/packages/shared/src/lib/license-keys/index.ts index f02d12d224..2033c394e5 100644 --- a/packages/shared/src/lib/license-keys/index.ts +++ b/packages/shared/src/lib/license-keys/index.ts @@ -26,7 +26,7 @@ export const LicenseKeyEntity = Type.Object({ isTrial: Type.Boolean(), key: Type.String(), ssoEnabled: Type.Boolean(), - gitSyncEnabled: Type.Boolean(), + environmentEnabled: Type.Boolean(), showPoweredBy: Type.Boolean(), embeddingEnabled: Type.Boolean(), auditLogEnabled: Type.Boolean(), diff --git a/packages/shared/src/lib/platform/platform.model.ts b/packages/shared/src/lib/platform/platform.model.ts index 49907a1ceb..383ed3009e 100644 --- a/packages/shared/src/lib/platform/platform.model.ts +++ b/packages/shared/src/lib/platform/platform.model.ts @@ -40,7 +40,7 @@ export const Platform = Type.Object({ filteredPieceBehavior: Type.Enum(FilteredPieceBehavior), smtp: Type.Optional(SMTPInformation), cloudAuthEnabled: Type.Boolean(), - gitSyncEnabled: Type.Boolean(), + environmentEnabled: Type.Boolean(), analyticsEnabled: Type.Boolean(), showPoweredBy: Type.Boolean(), auditLogEnabled: Type.Boolean(), diff --git a/packages/shared/src/lib/project/project.ts b/packages/shared/src/lib/project/project.ts index dcce6546d5..e966621581 100755 --- a/packages/shared/src/lib/project/project.ts +++ b/packages/shared/src/lib/project/project.ts @@ -64,6 +64,7 @@ export const Project = Type.Object({ notifyStatus: Type.Enum(NotificationStatus), platformId: ApId, externalId: Type.Optional(Type.String()), + releasesEnabled: Type.Boolean(), }) const projectAnalytics = Type.Object( From 155ac567dc4a54e5b85de8e4cecd3de2ca5bcbc7 Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Tue, 17 Dec 2024 12:54:25 +0000 Subject: [PATCH 07/57] fix: lint permissions --- .../app/components/dashboard-container.tsx | 4 ++-- packages/react-ui/src/app/router/index.tsx | 2 +- .../app/routes/settings/environment/index.tsx | 2 +- .../settings/environment/release-card.tsx | 22 ++++++++++--------- ...734429537542-AddReleasesEnablingBoolean.ts | 10 ++++----- ...734431436773-RenameGitSyncToEnvironment.ts | 10 ++++----- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/react-ui/src/app/components/dashboard-container.tsx b/packages/react-ui/src/app/components/dashboard-container.tsx index a1efc00888..5b11020a58 100644 --- a/packages/react-ui/src/app/components/dashboard-container.tsx +++ b/packages/react-ui/src/app/components/dashboard-container.tsx @@ -6,13 +6,13 @@ import { useEmbedding } from '@/components/embed-provider'; import { issueHooks } from '@/features/issues/hooks/issue-hooks'; import { useAuthorization } from '@/hooks/authorization-hooks'; import { platformHooks } from '@/hooks/platform-hooks'; +import { projectHooks } from '@/hooks/project-hooks'; import { isNil, Permission } from '@activepieces/shared'; import { authenticationSession } from '../../lib/authentication-session'; import { AllowOnlyLoggedInUserOnlyGuard } from './allow-logged-in-user-only-guard'; import { Sidebar, SidebarLink } from './sidebar'; -import { projectHooks } from '@/hooks/project-hooks'; type DashboardContainerProps = { children: React.ReactNode; @@ -23,7 +23,7 @@ export function DashboardContainer({ children }: DashboardContainerProps) { const { data: showIssuesNotification } = issueHooks.useIssuesNotification( platform.flowIssuesEnabled, ); - const { project } = projectHooks.useCurrentProject() + const { project } = projectHooks.useCurrentProject(); const { embedState } = useEmbedding(); const currentProjectId = authenticationSession.getProjectId(); diff --git a/packages/react-ui/src/app/router/index.tsx b/packages/react-ui/src/app/router/index.tsx index 68b75bd6c5..498a246faf 100644 --- a/packages/react-ui/src/app/router/index.tsx +++ b/packages/react-ui/src/app/router/index.tsx @@ -60,6 +60,7 @@ import { ProjectReleasesPage } from '../routes/project-release'; import { FlowRunPage } from '../routes/runs/id'; import AlertsPage from '../routes/settings/alerts'; import AppearancePage from '../routes/settings/appearance'; +import { EnvironmentPage } from '../routes/settings/environment'; import GeneralPage from '../routes/settings/general'; import TeamPage from '../routes/settings/team'; import { SignInPage } from '../routes/sign-in'; @@ -71,7 +72,6 @@ import { DefaultRoute } from './default-route'; import { FlagRouteGuard } from './flag-route-guard'; import { RoutePermissionGuard } from './permission-guard'; import { ProjectRouterWrapper } from './project-route-wrapper'; -import { EnvironmentPage } from '../routes/settings/environment'; const SettingsRerouter = () => { const { hash } = useLocation(); diff --git a/packages/react-ui/src/app/routes/settings/environment/index.tsx b/packages/react-ui/src/app/routes/settings/environment/index.tsx index 531163da2f..3c2b267f07 100644 --- a/packages/react-ui/src/app/routes/settings/environment/index.tsx +++ b/packages/react-ui/src/app/routes/settings/environment/index.tsx @@ -1,6 +1,5 @@ import { useMutation } from '@tanstack/react-query'; import { t } from 'i18next'; -import { useState } from 'react'; import LockedFeatureGuard from '@/app/components/locked-feature-guard'; import { Button } from '@/components/ui/button'; @@ -14,6 +13,7 @@ import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks'; import { platformHooks } from '@/hooks/platform-hooks'; import { authenticationSession } from '@/lib/authentication-session'; import { assertNotNullOrUndefined } from '@activepieces/shared'; + import { ReleaseCard } from './release-card'; const EnvironmentPage = () => { diff --git a/packages/react-ui/src/app/routes/settings/environment/release-card.tsx b/packages/react-ui/src/app/routes/settings/environment/release-card.tsx index edc9ff88ba..266e9fb76d 100644 --- a/packages/react-ui/src/app/routes/settings/environment/release-card.tsx +++ b/packages/react-ui/src/app/routes/settings/environment/release-card.tsx @@ -1,24 +1,24 @@ +import { useMutation } from '@tanstack/react-query'; import { t } from 'i18next'; import { Package } from 'lucide-react'; -import { Card } from '@/components/ui/card'; + import { Button } from '@/components/ui/button'; -import { toast } from '@/components/ui/use-toast'; -import { INTERNAL_ERROR_TOAST } from '@/components/ui/use-toast'; -import { useMutation } from '@tanstack/react-query'; +import { Card } from '@/components/ui/card'; +import { toast, INTERNAL_ERROR_TOAST } from '@/components/ui/use-toast'; import { projectHooks } from '@/hooks/project-hooks'; import { projectApi } from '@/lib/project-api'; const ReleaseCard = () => { - const { project, refetch } = projectHooks.useCurrentProject() + const { project, refetch } = projectHooks.useCurrentProject(); const { mutate } = useMutation({ mutationFn: () => { return projectApi.update(project.id, { releasesEnabled: !project.releasesEnabled, - }) + }); }, onSuccess: () => { - refetch() + refetch(); toast({ title: t('Releases Enabled'), description: t('You have successfully enabled releases'), @@ -29,12 +29,12 @@ const ReleaseCard = () => { toast(INTERNAL_ERROR_TOAST); }, }); - + return (
- +
{t('Releases')}
@@ -43,7 +43,9 @@ const ReleaseCard = () => {
- +
diff --git a/packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts b/packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts index 9d71930d4c..0830e83364 100644 --- a/packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts +++ b/packages/server/api/src/app/database/migration/postgres/1734429537542-AddReleasesEnablingBoolean.ts @@ -1,6 +1,6 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; -import { isNotOneOfTheseEditions } from "../../database-common"; -import { ApEdition } from "@activepieces/shared"; +import { ApEdition } from '@activepieces/shared' +import { MigrationInterface, QueryRunner } from 'typeorm' +import { isNotOneOfTheseEditions } from '../../database-common' export class AddReleasesEnablingBoolean1734429537542 implements MigrationInterface { name = 'AddReleasesEnablingBoolean1734429537542' @@ -12,7 +12,7 @@ export class AddReleasesEnablingBoolean1734429537542 implements MigrationInterfa await queryRunner.query(` ALTER TABLE "project" ADD "releasesEnabled" boolean NOT NULL DEFAULT false - `); + `) } public async down(queryRunner: QueryRunner): Promise { @@ -21,7 +21,7 @@ export class AddReleasesEnablingBoolean1734429537542 implements MigrationInterfa } await queryRunner.query(` ALTER TABLE "project" DROP COLUMN "releasesEnabled" - `); + `) } } diff --git a/packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts b/packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts index 0f6c12e764..a387d13ddb 100644 --- a/packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts +++ b/packages/server/api/src/app/database/migration/postgres/1734431436773-RenameGitSyncToEnvironment.ts @@ -1,6 +1,6 @@ -import { ApEdition } from "@activepieces/shared"; -import { MigrationInterface, QueryRunner } from "typeorm"; -import { isNotOneOfTheseEditions } from "../../database-common"; +import { ApEdition } from '@activepieces/shared' +import { MigrationInterface, QueryRunner } from 'typeorm' +import { isNotOneOfTheseEditions } from '../../database-common' export class RenameGitSyncToEnvironment1734431436773 implements MigrationInterface { name = 'RenameGitSyncToEnvironment1734431436773' @@ -12,7 +12,7 @@ export class RenameGitSyncToEnvironment1734431436773 implements MigrationInterfa await queryRunner.query(` ALTER TABLE "platform" RENAME COLUMN "gitSyncEnabled" TO "environmentEnabled" - `); + `) } public async down(queryRunner: QueryRunner): Promise { @@ -22,7 +22,7 @@ export class RenameGitSyncToEnvironment1734431436773 implements MigrationInterfa await queryRunner.query(` ALTER TABLE "platform" RENAME COLUMN "environmentEnabled" TO "gitSyncEnabled" - `); + `) } } From b45273b959b2e7d476029eecb8be0050b5d30675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=99=9F=E6=9D=B0?= <101547635+ChineseHamberger@users.noreply.github.com> Date: Thu, 19 Dec 2024 04:46:31 +0000 Subject: [PATCH 08/57] feat: add check-file-type action for file-helper --- .../pieces/community/file-helper/src/index.ts | 3 +- .../src/lib/actions/check-file-type.ts | 39 ++++++++ .../file-helper/src/lib/common/mimeTypes.ts | 88 +++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 packages/pieces/community/file-helper/src/lib/actions/check-file-type.ts create mode 100644 packages/pieces/community/file-helper/src/lib/common/mimeTypes.ts diff --git a/packages/pieces/community/file-helper/src/index.ts b/packages/pieces/community/file-helper/src/index.ts index ba06e50773..7d8cbd96d8 100644 --- a/packages/pieces/community/file-helper/src/index.ts +++ b/packages/pieces/community/file-helper/src/index.ts @@ -3,6 +3,7 @@ import { PieceCategory } from '@activepieces/shared'; import { readFileAction } from './lib/actions/read-file'; import { createFile } from './lib/actions/create-file'; import { changeFileEncoding } from './lib/actions/change-file-encoding'; +import { checkFileType} from './lib/actions/check-file-type'; export const filesHelper = createPiece({ displayName: 'Files Helper', @@ -12,6 +13,6 @@ export const filesHelper = createPiece({ logoUrl: 'https://cdn.activepieces.com/pieces/file-piece.svg', categories: [PieceCategory.CORE], authors: ['kishanprmr', 'MoShizzle', 'abuaboud', 'Seb-C'], - actions: [readFileAction, createFile, changeFileEncoding], + actions: [readFileAction, createFile, changeFileEncoding, checkFileType], triggers: [], }); diff --git a/packages/pieces/community/file-helper/src/lib/actions/check-file-type.ts b/packages/pieces/community/file-helper/src/lib/actions/check-file-type.ts new file mode 100644 index 0000000000..3ffdfb0257 --- /dev/null +++ b/packages/pieces/community/file-helper/src/lib/actions/check-file-type.ts @@ -0,0 +1,39 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { predefinedMimeTypes } from '../common/mimeTypes'; +import mime from 'mime-types'; + +export const checkFileType = createAction({ + name: 'checkFileType', + displayName: 'Check file type', + description: 'Check MIME type of a file and filter based on selected types', + props: { + file: Property.File({ + displayName: 'File to Check', + required: true, + }), + mimeTypes: Property.StaticDropdown({ + displayName: 'Select MIME Types', + required: true, + options: { + options: predefinedMimeTypes, + }, + description: 'Choose one or more MIME types to check against the file.', + }), + }, + async run(context) { + const file = context.propsValue.file; + + const selectedMimeTypes = context.propsValue.mimeTypes; + + // Determine the MIME type of the file + const fileType = file.extension ? mime.lookup(file.extension) || 'application/octet-stream' : 'application/octet-stream'; + + // Check if the file's MIME type matches any of the selected MIME types. + const isMatch = fileType && selectedMimeTypes.includes(fileType); + + return { + mimeType: fileType || 'unknown', + isMatch, + }; + }, +}); diff --git a/packages/pieces/community/file-helper/src/lib/common/mimeTypes.ts b/packages/pieces/community/file-helper/src/lib/common/mimeTypes.ts new file mode 100644 index 0000000000..f4840e8aaf --- /dev/null +++ b/packages/pieces/community/file-helper/src/lib/common/mimeTypes.ts @@ -0,0 +1,88 @@ +// Check out: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types +// Check out: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types +export const predefinedMimeTypes = [ + // important MIME types for Web developers + { label: 'Octet-stream', value: 'application/octet-stream' }, + // Textual Files + { label: 'Plain Text', value: 'text/plain' }, + { label: 'CSS Stylesheet', value: 'text/css' }, + { label: 'HTML Document', value: 'text/html' }, + { label: 'JavaScript', value: 'text/javascript' }, + { label: 'CSV File', value: 'text/csv' }, + { label: 'iCalendar Format', value: 'text/calendar' }, + // Image Types + { label: 'APNG Image', value: 'image/apng' }, + { label: 'AVIF Image', value: 'image/avif' }, + { label: 'GIF Image', value: 'image/gif' }, + { label: 'JPEG Image', value: 'image/jpeg' }, + { label: 'PNG Image', value: 'image/png' }, + { label: 'SVG Image', value: 'image/svg+xml' }, + { label: 'WebP Image', value: 'image/webp' }, + { label: 'BMP Image', value: 'image/bmp' }, + { label: 'Icon Format', value: 'image/vnd.microsoft.icon' }, + { label: 'TIFF Image', value: 'image/tiff' }, + // Audio Types + { label: 'AAC Audio', value: 'audio/aac' }, + { label: 'MP3 Audio', value: 'audio/mpeg' }, + { label: 'OGG Audio', value: 'audio/ogg' }, + { label: 'WAV Audio', value: 'audio/wav' }, + { label: 'FLAC Audio', value: 'audio/flac' }, + { label: 'MIDI Audio', value: 'audio/midi' }, + { label: 'WEBM Audio', value: 'audio/webm' }, + // Video Types + { label: 'MP4 Video', value: 'video/mp4' }, + { label: 'WebM Video', value: 'video/webm' }, + { label: 'OGG Video', value: 'video/ogg' }, + { label: 'AVI Video', value: 'video/x-msvideo' }, + { label: 'MPEG Video', value: 'video/mpeg' }, + { label: '3GPP Video', value: 'video/3gpp' }, + { label: '3GPP2 Video', value: 'video/3gpp2' }, + // Font Types + { label: 'EOT Font', value: 'application/vnd.ms-fontobject' }, + { label: 'OpenType Font', value: 'font/otf' }, + { label: 'TrueType Font', value: 'font/ttf' }, + { label: 'WOFF Font', value: 'font/woff' }, + { label: 'WOFF2 Font', value: 'font/woff2' }, + // Archive and Compressed Files + { label: 'BZip Archive', value: 'application/x-bzip' }, + { label: 'BZip2 Archive', value: 'application/x-bzip2' }, + { label: 'GZip Archive', value: 'application/gzip' }, + { label: 'RAR Archive', value: 'application/vnd.rar' }, + { label: 'TAR Archive', value: 'application/x-tar' }, + { label: 'ZIP Archive', value: 'application/zip' }, + { label: '7-Zip Archive', value: 'application/x-7z-compressed' }, + // Document Types + { label: 'AbiWord Document', value: 'application/x-abiword' }, + { label: 'PDF', value: 'application/pdf' }, + { label: 'Microsoft Word', value: 'application/msword' }, + { label: 'Microsoft Word (OpenXML)', value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }, + { label: 'Microsoft Excel', value: 'application/vnd.ms-excel' }, + { label: 'Microsoft Excel (OpenXML)', value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, + { label: 'Microsoft PowerPoint', value: 'application/vnd.ms-powerpoint' }, + { label: 'Microsoft PowerPoint (OpenXML)', value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }, + { label: 'OpenDocument Presentation', value: 'application/vnd.oasis.opendocument.presentation' }, + { label: 'OpenDocument Spreadsheet', value: 'application/vnd.oasis.opendocument.spreadsheet' }, + { label: 'OpenDocument Text', value: 'application/vnd.oasis.opendocument.text' }, + { label: 'Rich Text Format', value: 'application/rtf' }, + { label: 'Electronic Publication (EPUB)', value: 'application/epub+zip' }, + { label: 'Amazon Kindle eBook', value: 'application/vnd.amazon.ebook' }, + { label: 'XUL', value: 'application/vnd.mozilla.xul+xml' }, + { label: 'PHP Script', value: 'application/x-httpd-php' }, + { label: 'Java Archive (JAR)', value: 'application/java-archive' }, + { label: 'Microsoft Visio', value: 'application/vnd.visio' }, + { label: 'Apple Installer Package', value: 'application/vnd.apple.installer+xml' }, + // Multipart + { label: 'Form Data (multipart/form-data)', value: 'multipart/form-data' }, + { label: 'Partial Content (multipart/byteranges)', value: 'multipart/byteranges' }, + // Other Important MIME Types + { label: 'JSON', value: 'application/json' }, + { label: 'JSON-LD', value: 'application/ld+json' }, + { label: 'XML', value: 'application/xml' }, + { label: 'XHTML', value: 'application/xhtml+xml' }, + { label: 'C-Shell Script', value: 'application/x-csh' }, + { label: 'Bourne Shell Script', value: 'application/x-sh' }, + { label: 'FreeARC Archive', value: 'application/x-freearc' }, + { label: 'CD Audio', value: 'application/x-cdf' }, + { label: 'MPEG Transport Stream', value: 'video/mp2t' }, + { label: 'Opus Audio in Ogg Container', value: 'audio/opus' }, +] \ No newline at end of file From 1b0fcce41bfc3a5d610c4a10f5cf4612e5d13e26 Mon Sep 17 00:00:00 2001 From: AbdulTheActivePiecer Date: Thu, 19 Dec 2024 09:40:28 +0300 Subject: [PATCH 09/57] chore: add initial design for date/time picker --- .../src/components/ui/clock-picker.tsx | 73 +++++++ .../src/components/ui/date-picker-range.tsx | 136 +++++++++++- .../src/components/ui/time-period-select.tsx | 76 +++++++ .../src/components/ui/time-picker-input.tsx | 132 ++++++++++++ .../src/components/ui/time-picker-utils.ts | 204 ++++++++++++++++++ 5 files changed, 616 insertions(+), 5 deletions(-) create mode 100644 packages/react-ui/src/components/ui/clock-picker.tsx create mode 100644 packages/react-ui/src/components/ui/time-period-select.tsx create mode 100644 packages/react-ui/src/components/ui/time-picker-input.tsx create mode 100644 packages/react-ui/src/components/ui/time-picker-utils.ts diff --git a/packages/react-ui/src/components/ui/clock-picker.tsx b/packages/react-ui/src/components/ui/clock-picker.tsx new file mode 100644 index 0000000000..0de38f99f2 --- /dev/null +++ b/packages/react-ui/src/components/ui/clock-picker.tsx @@ -0,0 +1,73 @@ +'use client'; + +import * as React from 'react'; + +import { TimePeriodSelect } from './time-period-select'; +import { TimePickerInput } from './time-picker-input'; +import { Period } from './time-picker-utils'; + +interface TimePickerDemoProps { + date: Date | undefined; + setDate: (date: Date) => void; +} + +export function ClockPicker({ date, setDate }: TimePickerDemoProps) { + const [period, setPeriod] = React.useState(() => { + if (date) { + return date.getHours() >= 12 ? 'PM' : 'AM'; + } + return 'AM'; + }); + + const minuteRef = React.useRef(null); + const hourRef = React.useRef(null); + const secondRef = React.useRef(null); + const periodRef = React.useRef(null); + + return ( +
+
+ minuteRef.current?.focus()} + /> +
+
+ hourRef.current?.focus()} + onRightFocus={() => secondRef.current?.focus()} + /> +
+
+ minuteRef.current?.focus()} + onRightFocus={() => periodRef.current?.focus()} + /> +
+
+ secondRef.current?.focus()} + /> +
+
+ ); +} diff --git a/packages/react-ui/src/components/ui/date-picker-range.tsx b/packages/react-ui/src/components/ui/date-picker-range.tsx index 74976ade15..72d300ddfa 100644 --- a/packages/react-ui/src/components/ui/date-picker-range.tsx +++ b/packages/react-ui/src/components/ui/date-picker-range.tsx @@ -1,6 +1,7 @@ 'use client'; import { format, subDays, addDays } from 'date-fns'; +import { t } from 'i18next'; import { Calendar as CalendarIcon } from 'lucide-react'; import * as React from 'react'; import { DateRange } from 'react-day-picker'; @@ -21,8 +22,12 @@ import { } from '@/components/ui/select'; import { cn } from '@/lib/utils'; +import { ClockPicker } from './clock-picker'; +import { Label } from './label'; +import { Separator } from './separator'; + type DatePickerWithRangeProps = { - onChange: (date: DateRange) => void; + onChange: (date: DateRange | undefined) => void; className?: string; from?: string; to?: string; @@ -31,6 +36,49 @@ type DatePickerWithRangeProps = { presetType: 'past' | 'future'; }; +const applyTimeToDate = ({ + sourceDate, + targetDate, +}: { + sourceDate: Date; + targetDate: Date; +}): Date => { + // Extract time components from sourceDate + const hours = sourceDate.getHours(); + const minutes = sourceDate.getMinutes(); + const seconds = sourceDate.getSeconds(); + const milliseconds = sourceDate.getMilliseconds(); + + // Apply the time components to targetDate + targetDate.setHours(hours, minutes, seconds, milliseconds); + + return targetDate; // Return the updated targetDate +}; +const getInitialTimeDate = () => { + const now = new Date(); + const startDate = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + 0, + 0, + 0, + 0, + ); + const endDate = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + 23, + 59, + 59, + 999, + ); + return { + from: startDate, + to: endDate, + }; +}; export function DatePickerWithRange({ className, onChange, @@ -41,17 +89,43 @@ export function DatePickerWithRange({ presetType = 'past', }: DatePickerWithRangeProps) { const [date, setDate] = React.useState(); - + const [timeDate, setTimeDate] = React.useState( + getInitialTimeDate(), + ); React.useEffect(() => { setDate({ from: from ? new Date(from) : undefined, to: to ? new Date(to) : undefined, }); + const initialTimeDate = getInitialTimeDate(); + setTimeDate({ + from: from ? new Date(from) : initialTimeDate.from, + to: to ? new Date(to) : initialTimeDate.to, + }); }, [from, to]); const handleSelect = (selectedDate: DateRange | undefined) => { - setDate(selectedDate); if (selectedDate) { + const newDate = { + from: + selectedDate.from && timeDate.from + ? applyTimeToDate({ + sourceDate: timeDate.from, + targetDate: selectedDate.from, + }) + : undefined, + to: + selectedDate.to && timeDate.to + ? applyTimeToDate({ + sourceDate: timeDate.to, + targetDate: selectedDate.to, + }) + : undefined, + }; + setDate(newDate); + onChange(newDate); + } else { + setDate(selectedDate); onChange(selectedDate); } }; @@ -76,8 +150,10 @@ export function DatePickerWithRange({ default: newDate = { from: today, to: addDays(today, parseInt(value)) }; } - + newDate.from!.setHours(0, 0, 0, 0); + newDate.to!.setHours(23, 59, 59, 999); setDate(newDate); + setTimeDate(newDate); onChange(newDate); }; @@ -97,7 +173,7 @@ export function DatePickerWithRange({ {date?.from ? ( date.to ? ( <> - {format(date.from, 'LLL dd, y')} -{' '} + {format(date.from, 'LLL dd, y')}-{' '} {format(date.to, 'LLL dd, y')} ) : ( @@ -145,6 +221,56 @@ export function DatePickerWithRange({ toDate={maxDate} fromDate={minDate} /> + +
+
+ + { + if (date?.from) { + const fromWithCorrectedTime = applyTimeToDate({ + sourceDate: fromTime, + targetDate: date.from, + }); + setDate({ + from: fromWithCorrectedTime, + to: date.to, + }); + onChange({ + from: fromWithCorrectedTime, + to: date.to, + }); + } + setTimeDate({ ...timeDate, from: fromTime }); + }} + > +
+
+ + { + const toDate = date?.to ?? date?.from; + if (toDate) { + const toWithCorrectedTime = applyTimeToDate({ + sourceDate: toTime, + targetDate: toDate, + }); + setDate({ + from: date?.from, + to: toWithCorrectedTime, + }); + onChange({ + from: date?.from, + to: toWithCorrectedTime, + }); + } + setTimeDate({ ...timeDate, to: toTime }); + }} + > +
+
diff --git a/packages/react-ui/src/components/ui/time-period-select.tsx b/packages/react-ui/src/components/ui/time-period-select.tsx new file mode 100644 index 0000000000..42276a43b4 --- /dev/null +++ b/packages/react-ui/src/components/ui/time-period-select.tsx @@ -0,0 +1,76 @@ +'use client'; + +import * as React from 'react'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +import { Period, display12HourValue, setDateByType } from './time-picker-utils'; + +export interface PeriodSelectorProps { + period: Period; + setPeriod: (m: Period) => void; + date: Date | undefined; + setDate: (date: Date) => void; + onRightFocus?: () => void; + onLeftFocus?: () => void; +} + +export const TimePeriodSelect = React.forwardRef< + HTMLButtonElement, + PeriodSelectorProps +>(({ period, setPeriod, date, setDate, onLeftFocus, onRightFocus }, ref) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowRight') onRightFocus?.(); + if (e.key === 'ArrowLeft') onLeftFocus?.(); + }; + + const handleValueChange = (value: Period) => { + setPeriod(value); + + /** + * trigger an update whenever the user switches between AM and PM; + * otherwise user must manually change the hour each time + */ + if (date) { + const tempDate = new Date(date); + const hours = display12HourValue(date.getHours()); + setDate( + setDateByType( + tempDate, + hours.toString(), + '12hours', + period === 'AM' ? 'PM' : 'AM', + ), + ); + } + }; + + return ( +
+ +
+ ); +}); + +TimePeriodSelect.displayName = 'TimePeriodSelect'; diff --git a/packages/react-ui/src/components/ui/time-picker-input.tsx b/packages/react-ui/src/components/ui/time-picker-input.tsx new file mode 100644 index 0000000000..1f263bbf19 --- /dev/null +++ b/packages/react-ui/src/components/ui/time-picker-input.tsx @@ -0,0 +1,132 @@ +import React from 'react'; + +import { Input } from '@/components/ui/input'; +import { cn } from '@/lib/utils'; + +import { + Period, + TimePickerType, + getArrowByType, + getDateByType, + setDateByType, +} from './time-picker-utils'; + +export interface TimePickerInputProps + extends React.InputHTMLAttributes { + picker: TimePickerType; + date: Date | undefined; + setDate: (date: Date) => void; + period?: Period; + onRightFocus?: () => void; + onLeftFocus?: () => void; +} + +const TimePickerInput = React.forwardRef< + HTMLInputElement, + TimePickerInputProps +>( + ( + { + className, + type = 'tel', + value, + id, + name, + date = new Date(new Date().setHours(0, 0, 0, 0)), + setDate, + onChange, + onKeyDown, + picker, + period, + onLeftFocus, + onRightFocus, + ...props + }, + ref, + ) => { + const [flag, setFlag] = React.useState(false); + const [prevIntKey, setPrevIntKey] = React.useState('0'); + + /** + * allow the user to enter the second digit within 2 seconds + * otherwise start again with entering first digit + */ + React.useEffect(() => { + if (flag) { + const timer = setTimeout(() => { + setFlag(false); + }, 2000); + + return () => clearTimeout(timer); + } + }, [flag]); + + const calculatedValue = React.useMemo(() => { + return getDateByType(date, picker); + }, [date, picker]); + + const calculateNewValue = (key: string) => { + /* + * If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1. + * The second entered digit will break the condition and the value will be set to 10-12. + */ + if (picker === '12hours') { + if (flag && calculatedValue.slice(1, 2) === '1' && prevIntKey === '0') + return '0' + key; + } + + return !flag ? '0' + key : calculatedValue.slice(1, 2) + key; + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Tab') return; + e.preventDefault(); + if (e.key === 'ArrowRight') onRightFocus?.(); + if (e.key === 'ArrowLeft') onLeftFocus?.(); + if (['ArrowUp', 'ArrowDown'].includes(e.key)) { + const step = e.key === 'ArrowUp' ? 1 : -1; + const newValue = getArrowByType(calculatedValue, step, picker); + if (flag) setFlag(false); + const tempDate = new Date(date); + setDate(setDateByType(tempDate, newValue, picker, period)); + } + if (e.key >= '0' && e.key <= '9') { + if (picker === '12hours') setPrevIntKey(e.key); + + const newValue = calculateNewValue(e.key); + if (flag) onRightFocus?.(); + setFlag((prev) => !prev); + const tempDate = new Date(date); + setDate(setDateByType(tempDate, newValue, picker, period)); + } + }; + + return ( + { + e.preventDefault(); + onChange?.(e); + }} + type={type} + inputMode="decimal" + onKeyDown={(e) => { + onKeyDown?.(e); + handleKeyDown(e); + }} + {...props} + /> + ); + }, +); + +TimePickerInput.displayName = 'TimePickerInput'; + +export { TimePickerInput }; diff --git a/packages/react-ui/src/components/ui/time-picker-utils.ts b/packages/react-ui/src/components/ui/time-picker-utils.ts new file mode 100644 index 0000000000..5767f42dd1 --- /dev/null +++ b/packages/react-ui/src/components/ui/time-picker-utils.ts @@ -0,0 +1,204 @@ +/** + * regular expression to check for valid hour format (01-23) + */ +export function isValidHour(value: string) { + return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value); +} + +/** + * regular expression to check for valid 12 hour format (01-12) + */ +export function isValid12Hour(value: string) { + return /^(0[1-9]|1[0-2])$/.test(value); +} + +/** + * regular expression to check for valid minute format (00-59) + */ +export function isValidMinuteOrSecond(value: string) { + return /^[0-5][0-9]$/.test(value); +} + +type GetValidNumberConfig = { max: number; min?: number; loop?: boolean }; + +export function getValidNumber( + value: string, + { max, min = 0, loop = false }: GetValidNumberConfig, +) { + let numericValue = parseInt(value, 10); + + if (!isNaN(numericValue)) { + if (!loop) { + if (numericValue > max) numericValue = max; + if (numericValue < min) numericValue = min; + } else { + if (numericValue > max) numericValue = min; + if (numericValue < min) numericValue = max; + } + return numericValue.toString().padStart(2, '0'); + } + + return '00'; +} + +export function getValidHour(value: string) { + if (isValidHour(value)) return value; + return getValidNumber(value, { max: 23 }); +} + +export function getValid12Hour(value: string) { + if (isValid12Hour(value)) return value; + return getValidNumber(value, { min: 1, max: 12 }); +} + +export function getValidMinuteOrSecond(value: string) { + if (isValidMinuteOrSecond(value)) return value; + return getValidNumber(value, { max: 59 }); +} + +type GetValidArrowNumberConfig = { + min: number; + max: number; + step: number; +}; + +export function getValidArrowNumber( + value: string, + { min, max, step }: GetValidArrowNumberConfig, +) { + let numericValue = parseInt(value, 10); + if (!isNaN(numericValue)) { + numericValue += step; + return getValidNumber(String(numericValue), { min, max, loop: true }); + } + return '00'; +} + +export function getValidArrowHour(value: string, step: number) { + return getValidArrowNumber(value, { min: 0, max: 23, step }); +} + +export function getValidArrow12Hour(value: string, step: number) { + return getValidArrowNumber(value, { min: 1, max: 12, step }); +} + +export function getValidArrowMinuteOrSecond(value: string, step: number) { + return getValidArrowNumber(value, { min: 0, max: 59, step }); +} + +export function setMinutes(date: Date, value: string) { + const minutes = getValidMinuteOrSecond(value); + date.setMinutes(parseInt(minutes, 10)); + return date; +} + +export function setSeconds(date: Date, value: string) { + const seconds = getValidMinuteOrSecond(value); + date.setSeconds(parseInt(seconds, 10)); + return date; +} + +export function setHours(date: Date, value: string) { + const hours = getValidHour(value); + date.setHours(parseInt(hours, 10)); + return date; +} + +export function set12Hours(date: Date, value: string, period: Period) { + const hours = parseInt(getValid12Hour(value), 10); + const convertedHours = convert12HourTo24Hour(hours, period); + date.setHours(convertedHours); + return date; +} + +export type TimePickerType = 'minutes' | 'seconds' | 'hours' | '12hours'; +export type Period = 'AM' | 'PM'; + +export function setDateByType( + date: Date, + value: string, + type: TimePickerType, + period?: Period, +) { + switch (type) { + case 'minutes': + return setMinutes(date, value); + case 'seconds': + return setSeconds(date, value); + case 'hours': + return setHours(date, value); + case '12hours': { + if (!period) return date; + return set12Hours(date, value, period); + } + default: + return date; + } +} + +export function getDateByType(date: Date, type: TimePickerType) { + switch (type) { + case 'minutes': + return getValidMinuteOrSecond(String(date.getMinutes())); + case 'seconds': + return getValidMinuteOrSecond(String(date.getSeconds())); + case 'hours': + return getValidHour(String(date.getHours())); + case '12hours': { + const hours = display12HourValue(date.getHours()); + return getValid12Hour(String(hours)); + } + default: + return '00'; + } +} + +export function getArrowByType( + value: string, + step: number, + type: TimePickerType, +) { + switch (type) { + case 'minutes': + return getValidArrowMinuteOrSecond(value, step); + case 'seconds': + return getValidArrowMinuteOrSecond(value, step); + case 'hours': + return getValidArrowHour(value, step); + case '12hours': + return getValidArrow12Hour(value, step); + default: + return '00'; + } +} + +/** + * handles value change of 12-hour input + * 12:00 PM is 12:00 + * 12:00 AM is 00:00 + */ +export function convert12HourTo24Hour(hour: number, period: Period) { + if (period === 'PM') { + if (hour <= 11) { + return hour + 12; + } else { + return hour; + } + } else if (period === 'AM') { + if (hour === 12) return 0; + return hour; + } + return hour; +} + +/** + * time is stored in the 24-hour form, + * but needs to be displayed to the user + * in its 12-hour representation + */ +export function display12HourValue(hours: number) { + if (hours === 0 || hours === 12) return '12'; + if (hours >= 22) return `${hours - 12}`; + if (hours % 12 > 9) return `${hours}`; + return `0${hours % 12}`; +} From 854dcf1e546d0d02d97a168266ed337d792317be Mon Sep 17 00:00:00 2001 From: hazemadelkhalel Date: Thu, 19 Dec 2024 17:24:15 +0000 Subject: [PATCH 10/57] feat: add create project release from Git Repo --- packages/ee/shared/src/lib/git-repo/index.ts | 1 + .../app/routes/project-release/git-change.tsx | 50 ++++ .../project-release/git-release-dialog.tsx | 230 ++++++++++++++++++ .../src/app/routes/project-release/index.tsx | 161 +++++++++--- .../react-ui/src/components/delete-dialog.tsx | 4 +- .../lib/project-release-api.ts | 4 + packages/react-ui/src/lib/project-api.ts | 5 + ...1734418823028-CreateProjectReleaseTable.ts | 1 + .../src/app/ee/git-sync/git-sync.module.ts | 2 + .../src/app/ee/git-sync/git-sync.service.ts | 15 +- .../project-release.controller.ts | 23 +- .../project-release/project-release.entity.ts | 7 +- .../project-release.service.ts | 126 +++++++++- .../projects/platform-project-controller.ts | 27 ++ .../server/api/src/app/file/file.service.ts | 1 + .../server/api/test/helpers/mocks/index.ts | 2 + .../project-release/project-release.test.ts | 44 +--- .../src/lib/common/activepieces-error.ts | 6 + packages/shared/src/lib/file/index.ts | 4 +- .../project-release.request.ts | 20 +- .../lib/project-version/project-release.ts | 2 + 21 files changed, 637 insertions(+), 98 deletions(-) create mode 100644 packages/react-ui/src/app/routes/project-release/git-change.tsx create mode 100644 packages/react-ui/src/app/routes/project-release/git-release-dialog.tsx diff --git a/packages/ee/shared/src/lib/git-repo/index.ts b/packages/ee/shared/src/lib/git-repo/index.ts index 9e235735a4..56feade9fd 100644 --- a/packages/ee/shared/src/lib/git-repo/index.ts +++ b/packages/ee/shared/src/lib/git-repo/index.ts @@ -58,6 +58,7 @@ export type PullGitRepoFromProjectRequest = Static diff --git a/packages/react-ui/src/app/routes/project-release/git-change.tsx b/packages/react-ui/src/app/routes/project-release/git-change.tsx new file mode 100644 index 0000000000..222028bdf3 --- /dev/null +++ b/packages/react-ui/src/app/routes/project-release/git-change.tsx @@ -0,0 +1,50 @@ +import { UpdateIcon } from '@radix-ui/react-icons'; +import { t } from 'i18next'; +import { Minus, Plus } from 'lucide-react'; +import React from 'react'; + +import { + ProjectOperationType, + ProjectSyncPlanOperation, +} from '@activepieces/ee-shared'; + +type GitChangeProps = { + change: ProjectSyncPlanOperation; +}; +export const GitChange = React.memo(({ change }: GitChangeProps) => { + return ( + <> + {change.type === ProjectOperationType.CREATE_FLOW && ( +
+ + + {t('Create "{flowName}" Flow', { + flowName: change.flow.displayName, + })} + +
+ )} + {change.type === ProjectOperationType.UPDATE_FLOW && ( +
+ + + {t('Update "{flowName}" Flow', { + flowName: change.targetFlow.displayName, + })} + +
+ )} + {change.type === ProjectOperationType.DELETE_FLOW && ( +
+ + + {t('Delete "{flowName}" Flow', { + flowName: change.flow.displayName, + })} + +
+ )} + + ); +}); +GitChange.displayName = 'GitChange'; diff --git a/packages/react-ui/src/app/routes/project-release/git-release-dialog.tsx b/packages/react-ui/src/app/routes/project-release/git-release-dialog.tsx new file mode 100644 index 0000000000..ac83592049 --- /dev/null +++ b/packages/react-ui/src/app/routes/project-release/git-release-dialog.tsx @@ -0,0 +1,230 @@ +import { useMutation } from '@tanstack/react-query'; +import { t } from 'i18next'; +import React, { useEffect } from 'react'; + +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { INTERNAL_ERROR_TOAST, toast } from '@/components/ui/use-toast'; +import { gitSyncApi } from '@/features/git-sync/lib/git-sync-api'; +import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks'; +import { projectReleaseApi } from '@/features/project-version/lib/project-release-api'; +import { platformHooks } from '@/hooks/platform-hooks'; +import { authenticationSession } from '@/lib/authentication-session'; +import { + ProjectSyncPlan, + ProjectSyncPlanOperation, +} from '@activepieces/ee-shared'; +import { ProjectReleaseType } from '@activepieces/shared'; + +import { GitChange } from './git-change'; + +type GitReleaseDialogProps = { + open: boolean; + setOpen: (open: boolean) => void; + refetch: () => void; +}; + +const GitReleaseDialog = ({ + open, + setOpen, + refetch, +}: GitReleaseDialogProps) => { + const [syncPlan, setSyncPlan] = React.useState(null); + const [releaseName, setReleaseName] = React.useState(''); + const [releaseDescription, setReleaseDescription] = React.useState(''); + const [step, setStep] = React.useState(1); + const [isApplyingChanges, setIsApplyingChanges] = React.useState(false); + const [checkedOperations, setCheckedOperations] = React.useState< + Set + >(new Set()); + const { platform } = platformHooks.useCurrentPlatform(); + const { gitSync } = gitSyncHooks.useGitSync( + authenticationSession.getProjectId()!, + platform.environmentEnabled, + ); + + useEffect(() => { + if (syncPlan) { + setCheckedOperations(new Set(syncPlan.operations)); + } + }, [syncPlan]); + + const { mutate, isPending } = useMutation({ + mutationFn: async (dryRun: boolean) => { + if (releaseName.trim() === '') { + toast({ + title: t('Release name is required'), + description: t('Please enter a name for the release'), + duration: 2000, + }); + return; + } + if (gitSync) { + return gitSyncApi.pull(gitSync.id, { + dryRun, + selectedOperations: Array.from(checkedOperations).map( + (op) => op.flow.id, + ), + }); + } + }, + onSuccess: (plan) => { + if (step === 2) { + setOpen(false); + + return; + } + if (plan?.operations.length === 0) { + toast({ + title: t('No changes to pull'), + description: t('There are no changes to pull'), + duration: 3000, + }); + setSyncPlan(null); + } else if (plan) { + setSyncPlan(plan); + setStep(2); + } + }, + onError: (error) => { + console.error(error); + toast(INTERNAL_ERROR_TOAST); + }, + }); + + const { mutate: applyChanges } = useMutation({ + mutationFn: async () => { + setIsApplyingChanges(true); + if (gitSync) { + projectReleaseApi + .create({ + name: releaseName, + description: releaseDescription, + selectedOperations: Array.from(checkedOperations).map( + (op) => op.flow.id, + ), + repoId: gitSync.id, + type: ProjectReleaseType.GIT, + }) + .then(() => { + refetch(); + setOpen(false); + setIsApplyingChanges(false); + }); + } + }, + onError: (error) => { + console.error(error); + toast(INTERNAL_ERROR_TOAST); + }, + }); + + const renderChangesWithCheckboxes = () => ( +
+ {syncPlan?.operations.map((operation, index) => ( +
+ { + const newChecked = new Set(checkedOperations); + if (newChecked.has(operation)) { + newChecked.delete(operation); + } else { + newChecked.add(operation); + } + setCheckedOperations(newChecked); + }} + /> + +
+ ))} +
+ ); + + return ( + + + + {t('Create Git Release')} + + {t('These are the changes that will be applied to the project')} + + + {step === 1 ? ( +
+
+
+ + setReleaseName(e.target.value)} + placeholder={t('Required')} + required + /> +
+
+ +