mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(core): Use DTOs for source control push/pull requests (#12470)
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ecff3b732a
commit
1d86c4fdd2
@@ -36,6 +36,9 @@ export { UserUpdateRequestDto } from './user/user-update-request.dto';
|
||||
|
||||
export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto';
|
||||
|
||||
export { PullWorkFolderRequestDto } from './source-control/pull-work-folder-request.dto';
|
||||
export { PushWorkFolderRequestDto } from './source-control/push-work-folder-request.dto';
|
||||
|
||||
export { VariableListRequestDto } from './variables/variables-list-request.dto';
|
||||
export { CredentialsGetOneRequestQuery } from './credentials/credentials-get-one-request.dto';
|
||||
export { CredentialsGetManyRequestQuery } from './credentials/credentials-get-many-request.dto';
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { PullWorkFolderRequestDto } from '../pull-work-folder-request.dto';
|
||||
|
||||
describe('PullWorkFolderRequestDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with force',
|
||||
request: { force: true },
|
||||
},
|
||||
{
|
||||
name: 'without force',
|
||||
request: {},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = PullWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'invalid force type',
|
||||
request: {
|
||||
force: 'true', // Should be boolean
|
||||
},
|
||||
expectedErrorPath: ['force'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = PullWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { PushWorkFolderRequestDto } from '../push-work-folder-request.dto';
|
||||
|
||||
describe('PushWorkFolderRequestDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'complete valid push request with all fields',
|
||||
request: {
|
||||
force: true,
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file1.json',
|
||||
id: '1',
|
||||
name: 'File 1',
|
||||
type: 'workflow',
|
||||
status: 'modified',
|
||||
location: 'local',
|
||||
conflict: false,
|
||||
updatedAt: '2023-10-01T12:00:00Z',
|
||||
pushed: true,
|
||||
},
|
||||
],
|
||||
message: 'Initial commit',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'push request with only required fields',
|
||||
request: {
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file2.json',
|
||||
id: '2',
|
||||
name: 'File 2',
|
||||
type: 'credential',
|
||||
status: 'new',
|
||||
location: 'remote',
|
||||
conflict: true,
|
||||
updatedAt: '2023-10-02T12:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = PushWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'missing required fileNames field',
|
||||
request: {
|
||||
force: true,
|
||||
message: 'Initial commit',
|
||||
},
|
||||
expectedErrorPath: ['fileNames'],
|
||||
},
|
||||
{
|
||||
name: 'invalid fileNames type',
|
||||
request: {
|
||||
fileNames: 'not-an-array', // Should be an array
|
||||
},
|
||||
expectedErrorPath: ['fileNames'],
|
||||
},
|
||||
{
|
||||
name: 'invalid fileNames array element',
|
||||
request: {
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file4.json',
|
||||
id: '4',
|
||||
name: 'File 4',
|
||||
type: 'invalid-type', // Invalid type
|
||||
status: 'modified',
|
||||
location: 'local',
|
||||
conflict: false,
|
||||
updatedAt: '2023-10-04T12:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['fileNames', 0, 'type'],
|
||||
},
|
||||
{
|
||||
name: 'invalid force type',
|
||||
request: {
|
||||
force: 'true', // Should be boolean
|
||||
fileNames: [
|
||||
{
|
||||
file: 'file5.json',
|
||||
id: '5',
|
||||
name: 'File 5',
|
||||
type: 'workflow',
|
||||
status: 'modified',
|
||||
location: 'local',
|
||||
conflict: false,
|
||||
updatedAt: '2023-10-05T12:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['force'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = PushWorkFolderRequestDto.safeParse(request);
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class PullWorkFolderRequestDto extends Z.class({
|
||||
force: z.boolean().optional(),
|
||||
}) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { SourceControlledFileSchema } from '../../schemas/source-controlled-file.schema';
|
||||
|
||||
export class PushWorkFolderRequestDto extends Z.class({
|
||||
force: z.boolean().optional(),
|
||||
commitMessage: z.string().optional(),
|
||||
fileNames: z.array(SourceControlledFileSchema),
|
||||
}) {}
|
||||
@@ -10,9 +10,17 @@ export type { SendWorkerStatusMessage } from './push/worker';
|
||||
|
||||
export type { BannerName } from './schemas/bannerName.schema';
|
||||
export { passwordSchema } from './schemas/password.schema';
|
||||
|
||||
export {
|
||||
ProjectType,
|
||||
ProjectIcon,
|
||||
ProjectRole,
|
||||
ProjectRelation,
|
||||
} from './schemas/project.schema';
|
||||
|
||||
export {
|
||||
type SourceControlledFile,
|
||||
SOURCE_CONTROL_FILE_LOCATION,
|
||||
SOURCE_CONTROL_FILE_STATUS,
|
||||
SOURCE_CONTROL_FILE_TYPE,
|
||||
} from './schemas/source-controlled-file.schema';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const FileTypeSchema = z.enum(['credential', 'workflow', 'tags', 'variables', 'file']);
|
||||
export const SOURCE_CONTROL_FILE_TYPE = FileTypeSchema.Values;
|
||||
|
||||
const FileStatusSchema = z.enum([
|
||||
'new',
|
||||
'modified',
|
||||
'deleted',
|
||||
'created',
|
||||
'renamed',
|
||||
'conflicted',
|
||||
'ignored',
|
||||
'staged',
|
||||
'unknown',
|
||||
]);
|
||||
export const SOURCE_CONTROL_FILE_STATUS = FileStatusSchema.Values;
|
||||
|
||||
const FileLocationSchema = z.enum(['local', 'remote']);
|
||||
export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values;
|
||||
|
||||
export const SourceControlledFileSchema = z.object({
|
||||
file: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: FileTypeSchema,
|
||||
status: FileStatusSchema,
|
||||
location: FileLocationSchema,
|
||||
conflict: z.boolean(),
|
||||
updatedAt: z.string(),
|
||||
pushed: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type SourceControlledFile = z.infer<typeof SourceControlledFileSchema>;
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import mock from 'jest-mock-extended/lib/Mock';
|
||||
import { Cipher, type InstanceSettings } from 'n8n-core';
|
||||
@@ -9,7 +10,6 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
import { SourceControlExportService } from '../source-control-export.service.ee';
|
||||
import type { SourceControlledFile } from '../types/source-controlled-file';
|
||||
|
||||
// https://github.com/jestjs/jest/issues/4715
|
||||
function deepSpyOn<O extends object, M extends keyof O>(object: O, methodName: M) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import { constants as fsConstants, accessSync } from 'fs';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
} from '@/environments.ee/source-control/source-control-helper.ee';
|
||||
import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee';
|
||||
import type { SourceControlPreferences } from '@/environments.ee/source-control/types/source-control-preferences';
|
||||
import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file';
|
||||
import { License } from '@/license';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { rmSync } from 'fs';
|
||||
import { Credentials, InstanceSettings, Logger } from 'n8n-core';
|
||||
@@ -29,7 +30,6 @@ import type { ExportResult } from './types/export-result';
|
||||
import type { ExportableCredential } from './types/exportable-credential';
|
||||
import type { ExportableWorkflow } from './types/exportable-workflow';
|
||||
import type { ResourceOwner } from './types/resource-owner';
|
||||
import type { SourceControlledFile } from './types/source-controlled-file';
|
||||
import { VariablesService } from '../variables/variables.service.ee';
|
||||
|
||||
@Service()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import { generateKeyPairSync } from 'crypto';
|
||||
import { constants as fsConstants, mkdirSync, accessSync } from 'fs';
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
} from './constants';
|
||||
import type { KeyPair } from './types/key-pair';
|
||||
import type { KeyPairType } from './types/key-pair-type';
|
||||
import type { SourceControlledFile } from './types/source-controlled-file';
|
||||
|
||||
export function stringContainsExpression(testString: string): boolean {
|
||||
return /^=.*\{\{.*\}\}/.test(testString);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
@@ -37,7 +38,6 @@ import { getCredentialExportPath, getWorkflowExportPath } from './source-control
|
||||
import type { ExportableCredential } from './types/exportable-credential';
|
||||
import type { ResourceOwner } from './types/resource-owner';
|
||||
import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id';
|
||||
import type { SourceControlledFile } from './types/source-controlled-file';
|
||||
import { VariablesService } from '../variables/variables.service.ee';
|
||||
|
||||
@Service()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { PullWorkFolderRequestDto, PushWorkFolderRequestDto } from '@n8n/api-types';
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import express from 'express';
|
||||
import type { PullResult } from 'simple-git';
|
||||
|
||||
import { Get, Post, Patch, RestController, GlobalScope } from '@/decorators';
|
||||
import { Get, Post, Patch, RestController, GlobalScope, Body } from '@/decorators';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
|
||||
import { SOURCE_CONTROL_DEFAULT_BRANCH } from './constants';
|
||||
import {
|
||||
@@ -17,7 +20,6 @@ import type { ImportResult } from './types/import-result';
|
||||
import { SourceControlRequest } from './types/requests';
|
||||
import { SourceControlGetStatus } from './types/source-control-get-status';
|
||||
import type { SourceControlPreferences } from './types/source-control-preferences';
|
||||
import type { SourceControlledFile } from './types/source-controlled-file';
|
||||
|
||||
@RestController('/source-control')
|
||||
export class SourceControlController {
|
||||
@@ -164,19 +166,16 @@ export class SourceControlController {
|
||||
@Post('/push-workfolder', { middlewares: [sourceControlLicensedAndEnabledMiddleware] })
|
||||
@GlobalScope('sourceControl:push')
|
||||
async pushWorkfolder(
|
||||
req: SourceControlRequest.PushWorkFolder,
|
||||
req: AuthenticatedRequest,
|
||||
res: express.Response,
|
||||
@Body payload: PushWorkFolderRequestDto,
|
||||
): Promise<SourceControlledFile[]> {
|
||||
if (this.sourceControlPreferencesService.isBranchReadOnly()) {
|
||||
throw new BadRequestError('Cannot push onto read-only branch.');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sourceControlService.setGitUserDetails(
|
||||
`${req.user.firstName} ${req.user.lastName}`,
|
||||
req.user.email,
|
||||
);
|
||||
const result = await this.sourceControlService.pushWorkfolder(req.body);
|
||||
const result = await this.sourceControlService.pushWorkfolder(payload);
|
||||
res.statusCode = result.statusCode;
|
||||
return result.statusResult;
|
||||
} catch (error) {
|
||||
@@ -187,15 +186,12 @@ export class SourceControlController {
|
||||
@Post('/pull-workfolder', { middlewares: [sourceControlLicensedAndEnabledMiddleware] })
|
||||
@GlobalScope('sourceControl:pull')
|
||||
async pullWorkfolder(
|
||||
req: SourceControlRequest.PullWorkFolder,
|
||||
req: AuthenticatedRequest,
|
||||
res: express.Response,
|
||||
@Body payload: PullWorkFolderRequestDto,
|
||||
): Promise<SourceControlledFile[] | ImportResult | PullResult | undefined> {
|
||||
try {
|
||||
const result = await this.sourceControlService.pullWorkfolder({
|
||||
force: req.body.force,
|
||||
variables: req.body.variables,
|
||||
userId: req.user.id,
|
||||
});
|
||||
const result = await this.sourceControlService.pullWorkfolder(req.user.id, payload);
|
||||
res.statusCode = result.statusCode;
|
||||
return result.statusResult;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type {
|
||||
PullWorkFolderRequestDto,
|
||||
PushWorkFolderRequestDto,
|
||||
SourceControlledFile,
|
||||
} from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { Logger } from 'n8n-core';
|
||||
@@ -34,10 +39,7 @@ import type { ExportableCredential } from './types/exportable-credential';
|
||||
import type { ImportResult } from './types/import-result';
|
||||
import type { SourceControlGetStatus } from './types/source-control-get-status';
|
||||
import type { SourceControlPreferences } from './types/source-control-preferences';
|
||||
import type { SourceControllPullOptions } from './types/source-control-pull-work-folder';
|
||||
import type { SourceControlPushWorkFolder } from './types/source-control-push-work-folder';
|
||||
import type { SourceControlWorkflowVersionId } from './types/source-control-workflow-version-id';
|
||||
import type { SourceControlledFile } from './types/source-controlled-file';
|
||||
|
||||
@Service()
|
||||
export class SourceControlService {
|
||||
@@ -207,7 +209,7 @@ export class SourceControlService {
|
||||
return;
|
||||
}
|
||||
|
||||
async pushWorkfolder(options: SourceControlPushWorkFolder): Promise<{
|
||||
async pushWorkfolder(options: PushWorkFolderRequestDto): Promise<{
|
||||
statusCode: number;
|
||||
pushResult: PushResult | undefined;
|
||||
statusResult: SourceControlledFile[];
|
||||
@@ -299,7 +301,7 @@ export class SourceControlService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.gitService.commit(options.message ?? 'Updated Workfolder');
|
||||
await this.gitService.commit(options.commitMessage ?? 'Updated Workfolder');
|
||||
|
||||
const pushResult = await this.gitService.push({
|
||||
branch: this.sourceControlPreferencesService.getBranchName(),
|
||||
@@ -321,7 +323,8 @@ export class SourceControlService {
|
||||
}
|
||||
|
||||
async pullWorkfolder(
|
||||
options: SourceControllPullOptions,
|
||||
userId: User['id'],
|
||||
options: PullWorkFolderRequestDto,
|
||||
): Promise<{ statusCode: number; statusResult: SourceControlledFile[] }> {
|
||||
await this.sanityCheck();
|
||||
|
||||
@@ -345,7 +348,7 @@ export class SourceControlService {
|
||||
return true;
|
||||
});
|
||||
|
||||
if (options.force !== true) {
|
||||
if (!options.force) {
|
||||
const possibleConflicts = filteredResult?.filter(
|
||||
(file) => (file.conflict || file.status === 'modified') && file.type === 'workflow',
|
||||
);
|
||||
@@ -363,7 +366,7 @@ export class SourceControlService {
|
||||
);
|
||||
await this.sourceControlImportService.importWorkflowFromWorkFolder(
|
||||
workflowsToBeImported,
|
||||
options.userId,
|
||||
userId,
|
||||
);
|
||||
|
||||
const credentialsToBeImported = statusResult.filter(
|
||||
@@ -371,7 +374,7 @@ export class SourceControlService {
|
||||
);
|
||||
await this.sourceControlImportService.importCredentialsFromWorkFolder(
|
||||
credentialsToBeImported,
|
||||
options.userId,
|
||||
userId,
|
||||
);
|
||||
|
||||
const tagsToBeImported = statusResult.find((e) => e.type === 'tags');
|
||||
|
||||
@@ -5,9 +5,7 @@ import type { SourceControlDisconnect } from './source-control-disconnect';
|
||||
import type { SourceControlGenerateKeyPair } from './source-control-generate-key-pair';
|
||||
import type { SourceControlGetStatus } from './source-control-get-status';
|
||||
import type { SourceControlPreferences } from './source-control-preferences';
|
||||
import type { SourceControlPullWorkFolder } from './source-control-pull-work-folder';
|
||||
import type { SourceControlPush } from './source-control-push';
|
||||
import type { SourceControlPushWorkFolder } from './source-control-push-work-folder';
|
||||
import type { SourceControlSetBranch } from './source-control-set-branch';
|
||||
import type { SourceControlSetReadOnly } from './source-control-set-read-only';
|
||||
import type { SourceControlStage } from './source-control-stage';
|
||||
@@ -20,8 +18,6 @@ export declare namespace SourceControlRequest {
|
||||
type Stage = AuthenticatedRequest<{}, {}, SourceControlStage, {}>;
|
||||
type Push = AuthenticatedRequest<{}, {}, SourceControlPush, {}>;
|
||||
type Disconnect = AuthenticatedRequest<{}, {}, SourceControlDisconnect, {}>;
|
||||
type PushWorkFolder = AuthenticatedRequest<{}, {}, SourceControlPushWorkFolder, {}>;
|
||||
type PullWorkFolder = AuthenticatedRequest<{}, {}, SourceControlPullWorkFolder, {}>;
|
||||
type GetStatus = AuthenticatedRequest<{}, {}, {}, SourceControlGetStatus>;
|
||||
type GenerateKeyPair = AuthenticatedRequest<{}, {}, SourceControlGenerateKeyPair, {}>;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class SourceControlPullWorkFolder {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
force?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
importAfterPull?: boolean = true;
|
||||
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
files?: Set<string>;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
variables?: { [key: string]: string };
|
||||
}
|
||||
|
||||
export class SourceControllPullOptions {
|
||||
/** ID of user performing a source control pull. */
|
||||
userId: string;
|
||||
|
||||
force?: boolean;
|
||||
|
||||
variables?: { [key: string]: string };
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
import type { SourceControlledFile } from './source-controlled-file';
|
||||
|
||||
export class SourceControlPushWorkFolder {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
force?: boolean;
|
||||
|
||||
@IsString({ each: true })
|
||||
fileNames: SourceControlledFile[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
message?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
skipDiff?: boolean;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
export type SourceControlledFileStatus =
|
||||
| 'new'
|
||||
| 'modified'
|
||||
| 'deleted'
|
||||
| 'created'
|
||||
| 'renamed'
|
||||
| 'conflicted'
|
||||
| 'ignored'
|
||||
| 'staged'
|
||||
| 'unknown';
|
||||
export type SourceControlledFileLocation = 'local' | 'remote';
|
||||
export type SourceControlledFileType = 'credential' | 'workflow' | 'tags' | 'variables' | 'file';
|
||||
export type SourceControlledFile = {
|
||||
file: string;
|
||||
id: string;
|
||||
name: string;
|
||||
type: SourceControlledFileType;
|
||||
status: SourceControlledFileStatus;
|
||||
location: SourceControlledFileLocation;
|
||||
conflict: boolean;
|
||||
updatedAt: string;
|
||||
pushed?: boolean;
|
||||
};
|
||||
@@ -173,16 +173,6 @@ export interface IJsonSchema {
|
||||
required: string[];
|
||||
}
|
||||
|
||||
export class SourceControlPull {
|
||||
force?: boolean;
|
||||
|
||||
variables?: { [key: string]: string };
|
||||
}
|
||||
|
||||
export declare namespace PublicSourceControlRequest {
|
||||
type Pull = AuthenticatedRequest<{}, {}, SourceControlPull, {}>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// /audit
|
||||
// ----------------------------------
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PullWorkFolderRequestDto } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import type express from 'express';
|
||||
import type { StatusResult } from 'simple-git';
|
||||
@@ -10,15 +11,15 @@ import { SourceControlPreferencesService } from '@/environments.ee/source-contro
|
||||
import { SourceControlService } from '@/environments.ee/source-control/source-control.service.ee';
|
||||
import type { ImportResult } from '@/environments.ee/source-control/types/import-result';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
|
||||
import type { PublicSourceControlRequest } from '../../../types';
|
||||
import { globalScope } from '../../shared/middlewares/global.middleware';
|
||||
|
||||
export = {
|
||||
pull: [
|
||||
globalScope('sourceControl:pull'),
|
||||
async (
|
||||
req: PublicSourceControlRequest.Pull,
|
||||
req: AuthenticatedRequest,
|
||||
res: express.Response,
|
||||
): Promise<ImportResult | StatusResult | Promise<express.Response>> => {
|
||||
const sourceControlPreferencesService = Container.get(SourceControlPreferencesService);
|
||||
@@ -33,17 +34,14 @@ export = {
|
||||
.json({ status: 'Error', message: 'Source Control is not connected to a repository' });
|
||||
}
|
||||
try {
|
||||
const payload = PullWorkFolderRequestDto.parse(req.body);
|
||||
const sourceControlService = Container.get(SourceControlService);
|
||||
const result = await sourceControlService.pullWorkfolder({
|
||||
force: req.body.force,
|
||||
variables: req.body.variables,
|
||||
userId: req.user.id,
|
||||
});
|
||||
const result = await sourceControlService.pullWorkfolder(req.user.id, payload);
|
||||
|
||||
if (result.statusCode === 200) {
|
||||
Container.get(EventService).emit('source-control-user-pulled-api', {
|
||||
...getTrackingInformationFromPullResult(result.statusResult),
|
||||
forced: req.body.force ?? false,
|
||||
forced: payload.force ?? false,
|
||||
});
|
||||
return res.status(200).send(result.statusResult);
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { Cipher } from 'n8n-core';
|
||||
@@ -11,7 +12,6 @@ import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
||||
import { SourceControlImportService } from '@/environments.ee/source-control/source-control-import.service.ee';
|
||||
import type { ExportableCredential } from '@/environments.ee/source-control/types/exportable-credential';
|
||||
import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file';
|
||||
|
||||
import { mockInstance } from '../../shared/mocking';
|
||||
import { saveCredential } from '../shared/db/credentials';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import config from '@/config';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee';
|
||||
import { SourceControlService } from '@/environments.ee/source-control/source-control.service.ee';
|
||||
import type { SourceControlledFile } from '@/environments.ee/source-control/types/source-controlled-file';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import type {
|
||||
PullWorkFolderRequestDto,
|
||||
PushWorkFolderRequestDto,
|
||||
SourceControlledFile,
|
||||
} from '@n8n/api-types';
|
||||
import type { IRestApiContext } from '@/Interface';
|
||||
import type {
|
||||
SourceControlAggregatedFile,
|
||||
SourceControlPreferences,
|
||||
SourceControlStatus,
|
||||
SshKeyTypes,
|
||||
@@ -22,14 +25,14 @@ const createPreferencesRequestFn =
|
||||
|
||||
export const pushWorkfolder = async (
|
||||
context: IRestApiContext,
|
||||
data: IDataObject,
|
||||
data: PushWorkFolderRequestDto,
|
||||
): Promise<void> => {
|
||||
return await makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/push-workfolder`, data);
|
||||
};
|
||||
|
||||
export const pullWorkfolder = async (
|
||||
context: IRestApiContext,
|
||||
data: IDataObject,
|
||||
data: PullWorkFolderRequestDto,
|
||||
): Promise<void> => {
|
||||
return await makeRestApiRequest(context, 'POST', `${sourceControlApiRoot}/pull-workfolder`, data);
|
||||
};
|
||||
@@ -60,7 +63,7 @@ export const getAggregatedStatus = async (
|
||||
preferLocalVersion: boolean;
|
||||
verbose: boolean;
|
||||
} = { direction: 'push', preferLocalVersion: true, verbose: false },
|
||||
): Promise<SourceControlAggregatedFile[]> => {
|
||||
): Promise<SourceControlledFile[]> => {
|
||||
return await makeRestApiRequest(context, 'GET', `${sourceControlApiRoot}/get-status`, options);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { useLoadingService } from '@/composables/useLoadingService';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY } from '@/constants';
|
||||
import type { SourceControlAggregatedFile } from '@/types/sourceControl.types';
|
||||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
|
||||
defineProps<{
|
||||
isCollapsed: boolean;
|
||||
@@ -69,10 +69,8 @@ async function pullWorkfolder() {
|
||||
loadingService.setLoadingText(i18n.baseText('settings.sourceControl.loading.pull'));
|
||||
|
||||
try {
|
||||
const status: SourceControlAggregatedFile[] =
|
||||
((await sourceControlStore.pullWorkfolder(
|
||||
false,
|
||||
)) as unknown as SourceControlAggregatedFile[]) || [];
|
||||
const status: SourceControlledFile[] =
|
||||
((await sourceControlStore.pullWorkfolder(false)) as unknown as SourceControlledFile[]) || [];
|
||||
|
||||
const statusWithoutLocallyCreatedWorkflows = status.filter((file) => {
|
||||
return !(file.type === 'workflow' && file.status === 'created' && file.location === 'local');
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import Modal from './Modal.vue';
|
||||
import { SOURCE_CONTROL_PULL_MODAL_KEY } from '@/constants';
|
||||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import type { SourceControlAggregatedFile } from '@/types/sourceControl.types';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useLoadingService } from '@/composables/useLoadingService';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
@@ -10,9 +9,10 @@ import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
|
||||
const props = defineProps<{
|
||||
data: { eventBus: EventBus; status: SourceControlAggregatedFile[] };
|
||||
data: { eventBus: EventBus; status: SourceControlledFile[] };
|
||||
}>();
|
||||
|
||||
const incompleteFileTypes = ['variables', 'credential'];
|
||||
@@ -23,7 +23,7 @@ const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
|
||||
const files = ref<SourceControlAggregatedFile[]>(props.data.status || []);
|
||||
const files = ref<SourceControlledFile[]>(props.data.status || []);
|
||||
|
||||
const workflowFiles = computed(() => {
|
||||
return files.value.filter((file) => file.type === 'workflow');
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||
import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createEventBus } from 'n8n-design-system';
|
||||
import type { SourceControlAggregatedFile } from '@/types/sourceControl.types';
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { VIEWS } from '@/constants';
|
||||
@@ -71,7 +71,7 @@ describe('SourceControlPushModal', () => {
|
||||
});
|
||||
|
||||
it('should toggle checkboxes', async () => {
|
||||
const status: SourceControlAggregatedFile[] = [
|
||||
const status: SourceControlledFile[] = [
|
||||
{
|
||||
id: 'gTbbBkkYTnNyX1jD',
|
||||
name: 'My workflow 1',
|
||||
@@ -160,7 +160,7 @@ describe('SourceControlPushModal', () => {
|
||||
});
|
||||
|
||||
it('should push non workflow entities', async () => {
|
||||
const status: SourceControlAggregatedFile[] = [
|
||||
const status: SourceControlledFile[] = [
|
||||
{
|
||||
id: 'gTbbBkkYTnNyX1jD',
|
||||
name: 'credential',
|
||||
@@ -226,7 +226,7 @@ describe('SourceControlPushModal', () => {
|
||||
});
|
||||
|
||||
it('should auto select currentWorkflow', async () => {
|
||||
const status: SourceControlAggregatedFile[] = [
|
||||
const status: SourceControlledFile[] = [
|
||||
{
|
||||
id: 'gTbbBkkYTnNyX1jD',
|
||||
name: 'My workflow 1',
|
||||
@@ -276,7 +276,7 @@ describe('SourceControlPushModal', () => {
|
||||
|
||||
describe('filters', () => {
|
||||
it('should filter by name', async () => {
|
||||
const status: SourceControlAggregatedFile[] = [
|
||||
const status: SourceControlledFile[] = [
|
||||
{
|
||||
id: 'gTbbBkkYTnNyX1jD',
|
||||
name: 'My workflow 1',
|
||||
@@ -317,7 +317,7 @@ describe('SourceControlPushModal', () => {
|
||||
});
|
||||
|
||||
it('should filter by status', async () => {
|
||||
const status: SourceControlAggregatedFile[] = [
|
||||
const status: SourceControlledFile[] = [
|
||||
{
|
||||
id: 'gTbbBkkYTnNyX1jD',
|
||||
name: 'Created Workflow',
|
||||
@@ -375,7 +375,7 @@ describe('SourceControlPushModal', () => {
|
||||
});
|
||||
|
||||
it('should reset', async () => {
|
||||
const status: SourceControlAggregatedFile[] = [
|
||||
const status: SourceControlledFile[] = [
|
||||
{
|
||||
id: 'JIGKevgZagmJAnM6',
|
||||
name: 'Modified workflow',
|
||||
|
||||
@@ -31,16 +31,15 @@ import {
|
||||
N8nInfoTip,
|
||||
} from 'n8n-design-system';
|
||||
import {
|
||||
type SourceControlledFile,
|
||||
SOURCE_CONTROL_FILE_STATUS,
|
||||
SOURCE_CONTROL_FILE_TYPE,
|
||||
SOURCE_CONTROL_FILE_LOCATION,
|
||||
type SourceControlledFileStatus,
|
||||
type SourceControlAggregatedFile,
|
||||
} from '@/types/sourceControl.types';
|
||||
} from '@n8n/api-types';
|
||||
import { orderBy, groupBy } from 'lodash-es';
|
||||
|
||||
const props = defineProps<{
|
||||
data: { eventBus: EventBus; status: SourceControlAggregatedFile[] };
|
||||
data: { eventBus: EventBus; status: SourceControlledFile[] };
|
||||
}>();
|
||||
|
||||
const loadingService = useLoadingService();
|
||||
@@ -50,49 +49,48 @@ const i18n = useI18n();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const route = useRoute();
|
||||
|
||||
type SourceControlledFileStatus = SourceControlledFile['status'];
|
||||
|
||||
type Changes = {
|
||||
tags: SourceControlAggregatedFile[];
|
||||
variables: SourceControlAggregatedFile[];
|
||||
credentials: SourceControlAggregatedFile[];
|
||||
workflows: SourceControlAggregatedFile[];
|
||||
currentWorkflow?: SourceControlAggregatedFile;
|
||||
tags: SourceControlledFile[];
|
||||
variables: SourceControlledFile[];
|
||||
credentials: SourceControlledFile[];
|
||||
workflows: SourceControlledFile[];
|
||||
currentWorkflow?: SourceControlledFile;
|
||||
};
|
||||
|
||||
const classifyFilesByType = (
|
||||
files: SourceControlAggregatedFile[],
|
||||
currentWorkflowId?: string,
|
||||
): Changes =>
|
||||
const classifyFilesByType = (files: SourceControlledFile[], currentWorkflowId?: string): Changes =>
|
||||
files.reduce<Changes>(
|
||||
(acc, file) => {
|
||||
// do not show remote workflows that are not yet created locally during push
|
||||
if (
|
||||
file.location === SOURCE_CONTROL_FILE_LOCATION.REMOTE &&
|
||||
file.type === SOURCE_CONTROL_FILE_TYPE.WORKFLOW &&
|
||||
file.status === SOURCE_CONTROL_FILE_STATUS.CREATED
|
||||
file.location === SOURCE_CONTROL_FILE_LOCATION.remote &&
|
||||
file.type === SOURCE_CONTROL_FILE_TYPE.workflow &&
|
||||
file.status === SOURCE_CONTROL_FILE_STATUS.created
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.VARIABLES) {
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.variables) {
|
||||
acc.variables.push(file);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.TAGS) {
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.tags) {
|
||||
acc.tags.push(file);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.WORKFLOW && currentWorkflowId === file.id) {
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.workflow && currentWorkflowId === file.id) {
|
||||
acc.currentWorkflow = file;
|
||||
}
|
||||
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.WORKFLOW) {
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.workflow) {
|
||||
acc.workflows.push(file);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.CREDENTIAL) {
|
||||
if (file.type === SOURCE_CONTROL_FILE_TYPE.credential) {
|
||||
acc.credentials.push(file);
|
||||
return acc;
|
||||
}
|
||||
@@ -139,7 +137,7 @@ const toggleSelected = (id: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const maybeSelectCurrentWorkflow = (workflow?: SourceControlAggregatedFile) =>
|
||||
const maybeSelectCurrentWorkflow = (workflow?: SourceControlledFile) =>
|
||||
workflow && selectedChanges.value.add(workflow.id);
|
||||
onMounted(() => maybeSelectCurrentWorkflow(changes.value.currentWorkflow));
|
||||
|
||||
@@ -152,15 +150,15 @@ const resetFilters = () => {
|
||||
const statusFilterOptions: Array<{ label: string; value: SourceControlledFileStatus }> = [
|
||||
{
|
||||
label: 'New',
|
||||
value: SOURCE_CONTROL_FILE_STATUS.CREATED,
|
||||
value: SOURCE_CONTROL_FILE_STATUS.created,
|
||||
},
|
||||
{
|
||||
label: 'Modified',
|
||||
value: SOURCE_CONTROL_FILE_STATUS.MODIFIED,
|
||||
value: SOURCE_CONTROL_FILE_STATUS.modified,
|
||||
},
|
||||
{
|
||||
label: 'Deleted',
|
||||
value: SOURCE_CONTROL_FILE_STATUS.DELETED,
|
||||
value: SOURCE_CONTROL_FILE_STATUS.deleted,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -188,10 +186,10 @@ const filteredWorkflows = computed(() => {
|
||||
});
|
||||
|
||||
const statusPriority: Partial<Record<SourceControlledFileStatus, number>> = {
|
||||
[SOURCE_CONTROL_FILE_STATUS.MODIFIED]: 1,
|
||||
[SOURCE_CONTROL_FILE_STATUS.RENAMED]: 2,
|
||||
[SOURCE_CONTROL_FILE_STATUS.CREATED]: 3,
|
||||
[SOURCE_CONTROL_FILE_STATUS.DELETED]: 4,
|
||||
[SOURCE_CONTROL_FILE_STATUS.modified]: 1,
|
||||
[SOURCE_CONTROL_FILE_STATUS.renamed]: 2,
|
||||
[SOURCE_CONTROL_FILE_STATUS.created]: 3,
|
||||
[SOURCE_CONTROL_FILE_STATUS.deleted]: 4,
|
||||
} as const;
|
||||
const getPriorityByStatus = (status: SourceControlledFileStatus): number =>
|
||||
statusPriority[status] ?? 0;
|
||||
@@ -250,7 +248,7 @@ function close() {
|
||||
uiStore.closeModal(SOURCE_CONTROL_PUSH_MODAL_KEY);
|
||||
}
|
||||
|
||||
function renderUpdatedAt(file: SourceControlAggregatedFile) {
|
||||
function renderUpdatedAt(file: SourceControlledFile) {
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
|
||||
return i18n.baseText('settings.sourceControl.lastUpdated', {
|
||||
@@ -338,9 +336,9 @@ const getStatusTheme = (status: SourceControlledFileStatus) => {
|
||||
const statusToBadgeThemeMap: Partial<
|
||||
Record<SourceControlledFileStatus, 'success' | 'danger' | 'warning'>
|
||||
> = {
|
||||
[SOURCE_CONTROL_FILE_STATUS.CREATED]: 'success',
|
||||
[SOURCE_CONTROL_FILE_STATUS.DELETED]: 'danger',
|
||||
[SOURCE_CONTROL_FILE_STATUS.MODIFIED]: 'warning',
|
||||
[SOURCE_CONTROL_FILE_STATUS.created]: 'success',
|
||||
[SOURCE_CONTROL_FILE_STATUS.deleted]: 'danger',
|
||||
[SOURCE_CONTROL_FILE_STATUS.modified]: 'warning',
|
||||
} as const;
|
||||
return statusToBadgeThemeMap[status];
|
||||
};
|
||||
@@ -454,11 +452,11 @@ const getStatusTheme = (status: SourceControlledFileStatus) => {
|
||||
@update:model-value="toggleSelected(file.id)"
|
||||
>
|
||||
<span>
|
||||
<N8nText v-if="file.status === SOURCE_CONTROL_FILE_STATUS.DELETED" color="text-light">
|
||||
<span v-if="file.type === SOURCE_CONTROL_FILE_TYPE.WORKFLOW">
|
||||
<N8nText v-if="file.status === SOURCE_CONTROL_FILE_STATUS.deleted" color="text-light">
|
||||
<span v-if="file.type === SOURCE_CONTROL_FILE_TYPE.workflow">
|
||||
Deleted Workflow:
|
||||
</span>
|
||||
<span v-if="file.type === SOURCE_CONTROL_FILE_TYPE.CREDENTIAL">
|
||||
<span v-if="file.type === SOURCE_CONTROL_FILE_TYPE.credential">
|
||||
Deleted Credential:
|
||||
</span>
|
||||
<strong>{{ file.name || file.id }}</strong>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRootStore } from '@/stores/root.store';
|
||||
import * as vcApi from '@/api/sourceControl';
|
||||
import type { SourceControlPreferences, SshKeyTypes } from '@/types/sourceControl.types';
|
||||
import type { TupleToUnion } from '@/utils/typeHelpers';
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
|
||||
export const useSourceControlStore = defineStore('sourceControl', () => {
|
||||
const rootStore = useRootStore();
|
||||
@@ -39,23 +40,14 @@ export const useSourceControlStore = defineStore('sourceControl', () => {
|
||||
|
||||
const pushWorkfolder = async (data: {
|
||||
commitMessage: string;
|
||||
fileNames?: Array<{
|
||||
conflict: boolean;
|
||||
file: string;
|
||||
id: string;
|
||||
location: string;
|
||||
name: string;
|
||||
status: string;
|
||||
type: string;
|
||||
updatedAt?: string | undefined;
|
||||
}>;
|
||||
fileNames: SourceControlledFile[];
|
||||
force: boolean;
|
||||
}) => {
|
||||
state.commitMessage = data.commitMessage;
|
||||
await vcApi.pushWorkfolder(rootStore.restApiContext, {
|
||||
force: data.force,
|
||||
message: data.commitMessage,
|
||||
...(data.fileNames ? { fileNames: data.fileNames } : {}),
|
||||
commitMessage: data.commitMessage,
|
||||
fileNames: data.fileNames,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
import type { TupleToUnion } from '@/utils/typeHelpers';
|
||||
|
||||
export const SOURCE_CONTROL_FILE_STATUS = {
|
||||
NEW: 'new',
|
||||
MODIFIED: 'modified',
|
||||
DELETED: 'deleted',
|
||||
CREATED: 'created',
|
||||
RENAMED: 'renamed',
|
||||
CONFLICTED: 'conflicted',
|
||||
IGNORED: 'ignored',
|
||||
STAGED: 'staged',
|
||||
UNKNOWN: 'unknown',
|
||||
} as const;
|
||||
|
||||
export const SOURCE_CONTROL_FILE_LOCATION = {
|
||||
LOCAL: 'local',
|
||||
REMOTE: 'remote',
|
||||
} as const;
|
||||
|
||||
export const SOURCE_CONTROL_FILE_TYPE = {
|
||||
CREDENTIAL: 'credential',
|
||||
WORKFLOW: 'workflow',
|
||||
TAGS: 'tags',
|
||||
VARIABLES: 'variables',
|
||||
FILE: 'file',
|
||||
} as const;
|
||||
|
||||
export type SourceControlledFileStatus =
|
||||
(typeof SOURCE_CONTROL_FILE_STATUS)[keyof typeof SOURCE_CONTROL_FILE_STATUS];
|
||||
export type SourceControlledFileLocation =
|
||||
(typeof SOURCE_CONTROL_FILE_LOCATION)[keyof typeof SOURCE_CONTROL_FILE_LOCATION];
|
||||
export type SourceControlledFileType =
|
||||
(typeof SOURCE_CONTROL_FILE_TYPE)[keyof typeof SOURCE_CONTROL_FILE_TYPE];
|
||||
|
||||
export type SshKeyTypes = ['ed25519', 'rsa'];
|
||||
|
||||
export type SourceControlPreferences = {
|
||||
@@ -65,14 +33,3 @@ export interface SourceControlStatus {
|
||||
staged: string[];
|
||||
tracking: null;
|
||||
}
|
||||
|
||||
export interface SourceControlAggregatedFile {
|
||||
conflict: boolean;
|
||||
file: string;
|
||||
id: string;
|
||||
location: SourceControlledFileLocation;
|
||||
name: string;
|
||||
status: SourceControlledFileStatus;
|
||||
type: SourceControlledFileType;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user